Implemented switch mode menu

This commit is contained in:
jos 2016-09-29 14:19:01 +02:00
parent 88aed193c6
commit 873d5f8ae2
11 changed files with 267 additions and 46 deletions

View File

@ -397,11 +397,11 @@ export default class JSONNode extends Component {
if (this.state.menu) {
// hide context menu
JSONNode.hideContextMenu()
JSONNode.hideActionMenu()
}
else {
// hide any currently visible context menu
JSONNode.hideContextMenu()
JSONNode.hideActionMenu()
// show context menu
this.setState({
@ -419,11 +419,11 @@ export default class JSONNode extends Component {
if (this.state.appendMenu) {
// hide append context menu
JSONNode.hideContextMenu()
JSONNode.hideActionMenu()
}
else {
// hide any currently visible context menu
JSONNode.hideContextMenu()
JSONNode.hideActionMenu()
// show append context menu
this.setState({
@ -439,7 +439,7 @@ export default class JSONNode extends Component {
/**
* Singleton function to hide the currently visible context menu if any.
*/
static hideContextMenu () {
static hideActionMenu () {
if (activeContextMenu) {
activeContextMenu.setState({
menu: null,
@ -499,10 +499,11 @@ export default class JSONNode extends Component {
* Search is done based on the CSS class 'jsoneditor'
* @param event
* @return {*}
* @private
*/
// TODO: move to TreeMode?
static findRootElement (event) {
function isEditorElement (elem) {
// FIXME: this is a bit tricky. can we use a special attribute or something?
return elem.className.split(' ').indexOf('jsoneditor') !== -1
}

View File

@ -1,6 +1,7 @@
import { h, Component } from 'preact'
import { parseJSON } from './utils/jsonUtils'
import { jsonToData, dataToJson, patchData } from './jsonData'
import ModeButton from './menu/ModeButton'
export default class TextMode extends Component {
// TODO: define propTypes
@ -25,8 +26,19 @@ export default class TextMode extends Component {
class: 'jsoneditor-compact',
title: 'Compact the JSON document',
onClick: this.handleCompact
}),
// TODO: implement a button "Fix JSON"
h('div', {class: 'jsoneditor-vertical-menu-separator'}),
this.props.options.modes && h(ModeButton, {
open: this.state.modeMenuOpen,
modes: this.props.options.modes,
mode: this.props.mode,
onMode: this.props.onMode,
onClick: this.handleShowModeMenu
})
// TODO: implement a button "Fix JSON"
]),
h('div', {class: 'jsoneditor-contents'}, [

View File

@ -6,6 +6,7 @@ import {
duplicate, insert, append, remove, changeType, changeValue, changeProperty, sort
} from './actions'
import JSONNode from './JSONNode'
import ModeButton from './menu/ModeButton'
import { parseJSON } from './utils/jsonUtils'
const MAX_HISTORY_ITEMS = 1000 // maximum number of undo/redo items to be kept in memory
@ -16,11 +17,11 @@ export default class TreeMode extends Component {
constructor (props) {
super(props)
const expand = this.props.options && this.props.options.expand || TreeMode.expand
const expand = this.props.options.expand || TreeMode.expand
const data = jsonToData(this.props.data || {}, expand, [])
this.state = {
options: {
nodeOptions: {
name: null
},
@ -28,7 +29,7 @@ export default class TreeMode extends Component {
history: [data],
historyIndex: 0,
events: {
onChangeProperty: this.handleChangeProperty,
onChangeValue: this.handleChangeValue,
@ -49,38 +50,14 @@ export default class TreeMode extends Component {
render (props, state) {
// TODO: make mode tree dynamic
return h('div', {class: 'jsoneditor jsoneditor-mode-tree'}, [
h('div', {class: 'jsoneditor-menu'}, [
h('button', {
class: 'jsoneditor-expand-all',
title: 'Expand all objects and arrays',
onClick: this.handleExpandAll
}),
h('button', {
class: 'jsoneditor-collapse-all',
title: 'Collapse all objects and arrays',
onClick: this.handleCollapseAll
}),
h('div', {class: 'jsoneditor-vertical-menu-separator'}),
h('button', {
class: 'jsoneditor-undo',
title: 'Undo last action',
disabled: !this.canUndo(),
onClick: this.undo
}),
h('button', {
class: 'jsoneditor-redo',
title: 'Redo',
disabled: !this.canRedo(),
onClick: this.redo
})
]),
this.renderMenu(),
h('div', {class: 'jsoneditor-contents jsoneditor-tree-contents', onClick: JSONNode.hideContextMenu}, [
h('div', {class: 'jsoneditor-contents jsoneditor-tree-contents', onClick: this.handleHideMenus}, [
h('ul', {class: 'jsoneditor-list jsoneditor-root'}, [
h(JSONNode, {
data: state.data,
events: state.events,
options: state.options,
options: state.nodeOptions,
parent: null,
prop: null
})
@ -89,6 +66,51 @@ export default class TreeMode extends Component {
])
}
renderMenu () {
return h('div', {class: 'jsoneditor-menu'}, [
h('button', {
class: 'jsoneditor-expand-all',
title: 'Expand all objects and arrays',
onClick: this.handleExpandAll
}),
h('button', {
class: 'jsoneditor-collapse-all',
title: 'Collapse all objects and arrays',
onClick: this.handleCollapseAll
}),
h('div', {class: 'jsoneditor-vertical-menu-separator'}),
h('div', {style: 'display:inline-block'}, [
h('button', {
class: 'jsoneditor-undo',
title: 'Undo last action',
disabled: !this.canUndo(),
onClick: this.undo
}),
]),
h('button', {
class: 'jsoneditor-redo',
title: 'Redo',
disabled: !this.canRedo(),
onClick: this.redo
}),
h('div', {class: 'jsoneditor-vertical-menu-separator'}),
this.props.options.modes && h(ModeButton, {
modes: this.props.options.modes,
mode: this.props.mode,
onMode: this.props.onMode
})
])
}
/** @private */
handleHideMenus = () => {
JSONNode.hideActionMenu()
}
/** @private */
handleChangeValue = (path, value) => {
this.handlePatch(changeValue(this.state.data, path, value))
@ -184,7 +206,7 @@ export default class TreeMode extends Component {
* @private
*/
emitOnChange (patch, revert) {
if (this.props.options && this.props.options.onChange) {
if (this.props.options.onChange) {
this.props.options.onChange(patch, revert)
}
}
@ -275,7 +297,7 @@ export default class TreeMode extends Component {
const data = jsonToData(json, options.expand || TreeMode.expand, [])
this.setState({
options: setIn(this.state.options, ['name'], name),
nodeOptions: setIn(this.state.nodeOptions, ['name'], name),
data,
// TODO: do we want to keep history when .set(json) is called?
@ -305,7 +327,7 @@ export default class TreeMode extends Component {
* @return {string} text
*/
getText () {
const indentation = this.props.options && this.props.options.indentation || 2
const indentation = this.props.options.indentation || 2
return JSON.stringify(this.get(), null, indentation)
}

View File

@ -37,10 +37,14 @@
window.patch = patch
window.revert = revert
},
onChangeMode: function (mode, prevMode) {
console.log('switched mode from', prevMode, 'to', mode)
},
onError: function (err) {
console.error(err)
alert(err)
},
modes: ['text', 'tree'],
indentation: 4
}
const editor = jsoneditor(container, options)

View File

@ -17,7 +17,7 @@ const modes = {
* @return {Object}
* @constructor
*/
function jsoneditor (container, options) {
function jsoneditor (container, options = {}) {
const editor = {
isJSONEditor: true,
@ -132,7 +132,11 @@ function jsoneditor (container, options) {
// create new component
element = render(
h(constructor, {options: editor._options}),
h(constructor, {
mode,
options: editor._options,
onMode: editor.setMode
}),
editor._container)
// set JSON (this can throw an error)
@ -150,9 +154,14 @@ function jsoneditor (container, options) {
editor._element.parentNode.removeChild(editor._element)
}
const prevMode = editor._mode
editor._mode = mode
editor._element = element
editor._component = element._component
if (editor._options.onChangeMode && prevMode) {
editor._options.onChangeMode(mode, prevMode)
}
}
else {
// remove the just created component (where setText failed)

View File

@ -306,6 +306,11 @@ div.jsoneditor-contextmenu.jsoneditor-contextmenu-top {
bottom: 20px;
}
div.jsoneditor-modemenu.jsoneditor-modemenu {
top: 26px;
left: 0;
}
div.jsoneditor-menu-item {
line-height: 0;
font-size: 0;
@ -500,6 +505,28 @@ button.jsoneditor-type-Array.jsoneditor-selected span.jsoneditor-icon {
background-position: -96px 0;
}
div.jsoneditor-modes {
position: relative;
display: inline-block;
vertical-align: top;
button {
background: none;
width: auto;
padding: 2px 6px;
}
button.jsoneditor-type-modes {
width: 120px;
height: auto;
padding: 2px 6px;
border-radius: 0;
&:hover {
border: none;
}
}
}
textarea.jsoneditor-text {
width: 100%;

View File

@ -20,8 +20,6 @@ export default class Menu extends Component {
expanding: null, // menu index of expanding menu item
collapsing: null // menu index of collapsing menu item
}
this.renderMenuItem = this.renderMenuItem.bind(this)
}
/**
@ -45,7 +43,7 @@ export default class Menu extends Component {
)
}
renderMenuItem (item, index) {
renderMenuItem = (item, index) => {
if (item.type === 'separator') {
return h('div', {class: 'jsoneditor-menu-separator'})
}

41
src/menu/ModeButton.js Normal file
View File

@ -0,0 +1,41 @@
import { h, Component } from 'preact'
import ModeMenu from './ModeMenu'
import { toCapital } from '../utils/stringUtils'
export default class ModeButton extends Component {
constructor (props) {
super (props)
this.state = {
open: false // whether the menu is open or not
}
}
/**
* @param {{modes: string[], mode: string, onMode: function}} props
* @param state
* @return {*}
*/
render (props, state) {
return h('div', {class: 'jsoneditor-modes'}, [
h('button', {
title: 'Switch mode',
onClick: this.handleOpen
}, `${toCapital(props.mode)} \u25BC`),
h(ModeMenu, {
...props,
open: state.open,
onRequestClose: this.handleRequestClose
})
])
}
handleOpen = () => {
this.setState({open: true})
}
handleRequestClose = () => {
this.setState({open: false})
}
}

97
src/menu/ModeMenu.js Normal file
View File

@ -0,0 +1,97 @@
import { h, Component } from 'preact'
import { toCapital } from '../utils/stringUtils'
export default class ModeMenu extends Component {
/**
* @param {{open, modes, mode, onMode}} props
* @param {Obect} state
* @return {JSX.Element}
*/
render (props, state) {
if (props.open) {
const items = props.modes.map(mode => {
return h('button', {
title: `Switch to ${mode} mode`,
class: 'jsoneditor-menu-button jsoneditor-type-modes' +
((mode === props.mode) ? ' jsoneditor-selected' : ''),
onClick: () => {
props.onMode(mode)
this.setState({ open: false })
}
}, toCapital(mode))
})
return h('div', {
class: 'jsoneditor-contextmenu jsoneditor-modemenu',
'isnodemenu': 'true',
}, items)
}
else {
return null
}
}
componentDidMount () {
this.updateRequestCloseListener()
}
componentDidUpdate () {
this.updateRequestCloseListener()
}
componentWillUnmount () {
this.removeRequestCloseListener()
}
updateRequestCloseListener () {
if (this.props.open) {
this.addRequestCloseListener()
}
else {
this.removeRequestCloseListener()
}
}
addRequestCloseListener () {
if (!this.handleRequestClose) {
// Attach event listener on next tick, else the current click to open
// the menu will immediately result in requestClose event as well
setTimeout(() => {
this.handleRequestClose = (event) => {
if (!ModeMenu.inNodeMenu(event.target)) {
this.props.onRequestClose()
}
}
window.addEventListener('click', this.handleRequestClose)
}, 0)
}
}
removeRequestCloseListener () {
if (this.handleRequestClose) {
window.removeEventListener('click', this.handleRequestClose)
this.handleRequestClose = null
}
}
/**
* Test whether any of the parent nodes of this element is the root of the
* NodeMenu (has an attribute isNodeMenu:true)
* @param elem
* @return {boolean}
*/
static inNodeMenu (elem) {
let parent = elem
while (parent && parent.getAttribute) {
if (parent.getAttribute('isnodemenu')) {
return true
}
parent = parent.parentNode
}
return false
}
handleRequestClose = null
}

View File

@ -32,6 +32,7 @@
*
* @typedef {{
* mode: 'tree' | 'text',
* modes: string[],
* indentation: number | string,
* onChange: function (patch: JSONPatch, revert: JSONPatch),
* onError: function (err: Error)

View File

@ -109,4 +109,13 @@ export function findUniqueName (name, invalidNames) {
}
return validName
}
}
/**
* Transform a text into lower case with the first character upper case
* @param {string} text
* @return {string}
*/
export function toCapital(text) {
return text[0].toUpperCase() + text.substr(1).toLowerCase()
}