Refactored menus

This commit is contained in:
jos 2016-09-29 10:33:32 +02:00
parent 339b73fe51
commit 88aed193c6
5 changed files with 272 additions and 185 deletions

View File

@ -1,6 +1,7 @@
import { h, Component } from 'preact'
import ContextMenu from './ContextMenu'
import ActionMenu from './menu/ActionMenu'
import AppendActionMenu from './menu/AppendActionMenu'
import { escapeHTML, unescapeHTML } from './utils/stringUtils'
import { getInnerText } from './utils/domUtils'
import { stringConvert, valueType, isUrl } from './utils/typeUtils'
@ -56,7 +57,7 @@ export default class JSONNode extends Component {
const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-object'}, [
this.renderExpandButton(),
this.renderContextMenuButton(),
this.renderActionMenuButton(),
this.renderProperty(prop, data, options),
this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`)
])
@ -92,7 +93,7 @@ export default class JSONNode extends Component {
const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
this.renderExpandButton(),
this.renderContextMenuButton(),
this.renderActionMenuButton(),
this.renderProperty(prop, data, options),
this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`)
])
@ -126,7 +127,7 @@ export default class JSONNode extends Component {
return h('li', {}, [
h('div', {class: 'jsoneditor-node'}, [
this.renderPlaceholder(),
this.renderContextMenuButton(),
this.renderActionMenuButton(),
this.renderProperty(prop, data, options),
this.renderSeparator(),
this.renderValue(data.value)
@ -279,12 +280,12 @@ export default class JSONNode extends Component {
)
}
renderContextMenuButton () {
renderActionMenuButton () {
const className = 'jsoneditor-button jsoneditor-contextmenu' +
(this.state.menu ? ' jsoneditor-visible' : '')
return h('div', {class: 'jsoneditor-button-container'}, [
this.renderContextMenu(this.state.menu),
this.renderActionMenu(),
h('button', {class: className, onClick: this.handleContextMenu})
])
}
@ -299,187 +300,33 @@ export default class JSONNode extends Component {
])
}
renderContextMenu () {
if (!this.state.menu) {
renderActionMenu () {
if (this.state.menu) {
return h(ActionMenu, {
anchor: this.state.menu.anchor,
root: this.state.menu.root,
path: this.getPath(),
type: this.props.data.type,
events: this.props.events
})
}
else {
return null
}
const {anchor, root} = this.state.menu
const path = this.getPath()
const hasParent = this.props.parent !== null
const type = this.props.data.type
const events = this.props.events
const items = [] // array with menu items
items.push({
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: () => events.onChangeType(path, 'value')
},
{
text: 'Array',
className: 'jsoneditor-type-Array' + (type == 'Array' ? ' jsoneditor-selected' : ''),
title: TYPE_TITLES.array,
click: () => events.onChangeType(path, 'Array')
},
{
text: 'Object',
className: 'jsoneditor-type-Object' + (type == 'Object' ? ' jsoneditor-selected' : ''),
title: TYPE_TITLES.object,
click: () => events.onChangeType(path, 'Object')
},
{
text: 'String',
className: 'jsoneditor-type-string' + (type == 'string' ? ' jsoneditor-selected' : ''),
title: TYPE_TITLES.string,
click: () => events.onChangeType(path, 'string')
}
]
})
if (type === 'Array' || type === 'Object') {
var direction = ((this.sortOrder == 'asc') ? 'desc': 'asc')
items.push({
text: 'Sort',
title: 'Sort the childs of this ' + TYPE_TITLES.type,
className: 'jsoneditor-sort-' + direction,
click: () => events.onSort(path),
submenu: [
{
text: 'Ascending',
className: 'jsoneditor-sort-asc',
title: 'Sort the childs of this ' + TYPE_TITLES.type + ' in ascending order',
click: () => events.onSort(path, 'asc')
},
{
text: 'Descending',
className: 'jsoneditor-sort-desc',
title: 'Sort the childs of this ' + TYPE_TITLES.type +' in descending order',
click: () => events.onSort(path, 'desc')
}
]
})
}
if (hasParent) {
if (items.length) {
// create a separator
items.push({
'type': 'separator'
})
}
// create insert button
items.push({
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: () => events.onInsert(path, 'value'),
submenu: [
{
text: 'Value',
className: 'jsoneditor-type-value',
title: TYPE_TITLES.value,
click: () => events.onInsert(path, 'value')
},
{
text: 'Array',
className: 'jsoneditor-type-Array',
title: TYPE_TITLES.array,
click: () => events.onInsert(path, 'Array')
},
{
text: 'Object',
className: 'jsoneditor-type-Object',
title: TYPE_TITLES.object,
click: () => events.onInsert(path, 'Object')
},
{
text: 'String',
className: 'jsoneditor-type-string',
title: TYPE_TITLES.string,
click: () => events.onInsert(path, 'string')
}
]
})
// create duplicate button
items.push({
text: 'Duplicate',
title: 'Duplicate this item (Ctrl+D)',
className: 'jsoneditor-duplicate',
click: () => events.onDuplicate(path)
})
// create remove button
items.push({
text: 'Remove',
title: 'Remove this item (Ctrl+Del)',
className: 'jsoneditor-remove',
click: () => events.onRemove(path)
})
}
// TODO: implement a hook to adjust the context menu
return h(ContextMenu, {anchor, root, items})
}
renderAppendContextMenu () {
if (!this.state.appendMenu) {
if (this.state.appendMenu) {
return h(AppendActionMenu, {
anchor: this.state.menu.anchor,
root: this.state.menu.root,
path: this.getPath(),
events: this.props.events
})
}
else {
return null
}
const {anchor, root} = this.state.appendMenu
const path = this.getPath()
const events = this.props.events
const items = [] // array with menu items
// create insert button
items.push({
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: () => events.onAppend(path, 'value'),
submenu: [
{
text: 'Value',
className: 'jsoneditor-type-value',
title: TYPE_TITLES.value,
click: () => events.onAppend(path, 'value')
},
{
text: 'Array',
className: 'jsoneditor-type-Array',
title: TYPE_TITLES.array,
click: () => events.onAppend(path, 'Array')
},
{
text: 'Object',
className: 'jsoneditor-type-Object',
title: TYPE_TITLES.object,
click: () => events.onAppend(path, 'Object')
},
{
text: 'String',
className: 'jsoneditor-type-string',
title: TYPE_TITLES.string,
click: () => events.onAppend(path, 'string')
}
]
})
// TODO: implement a hook to adjust the context menu
return h(ContextMenu, {anchor, root, items})
}
shouldComponentUpdate(nextProps, nextState) {

42
src/menu/ActionMenu.js Normal file
View File

@ -0,0 +1,42 @@
import { h, Component } from 'preact'
import Menu from './Menu'
import {
createChangeType, createSort,
createSeparator,
createInsert, createDuplicate, createRemove
} from './entries'
export default class ActionMenu extends Component {
/**
* @param {{anchor, root, path, type, events}} props
* @param state
* @return {JSX.Element}
*/
render (props, state) {
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))
}
// TODO: implement a hook to adjust the action menu
return h(Menu, {
anchor: props.anchor,
root: props.root,
items
})
}
}

View File

@ -0,0 +1,24 @@
import { h, Component } from 'preact'
import Menu from './Menu'
import { createAppend } from './entries'
export default class AppendActionMenu extends Component {
/**
* @param {{anchor, root, path, events}} props
* @param state
* @return {JSX.Element}
*/
render (props, state) {
const items = [
createAppend(props.path, props.events.onAppend)
]
// TODO: implement a hook to adjust the action menu
return h(Menu, {
anchor: props.anchor,
root: props.root,
items
})
}
}

View File

@ -2,7 +2,7 @@ import { h, Component } from 'preact'
export let CONTEXT_MENU_HEIGHT = 240
export default class ContextMenu extends Component {
export default class Menu extends Component {
constructor(props) {
super(props)
@ -24,8 +24,13 @@ export default class ContextMenu extends Component {
this.renderMenuItem = this.renderMenuItem.bind(this)
}
render () {
if (!this.props.items) {
/**
* @param {{items: Array}} props
* @param state
* @return {*}
*/
render (props, state) {
if (!props.items) {
return null
}
@ -36,7 +41,7 @@ export default class ContextMenu extends Component {
((this.state.orientation === 'top') ? 'jsoneditor-contextmenu-top' : 'jsoneditor-contextmenu-bottom')
return h('div', {class: className},
this.props.items.map(this.renderMenuItem)
props.items.map(this.renderMenuItem)
)
}

169
src/menu/entries.js Normal file
View File

@ -0,0 +1,169 @@
// 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) {
var 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'
}
}