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

View File

@ -1,6 +1,7 @@
import { h, Component } from 'preact' import { h, Component } from 'preact'
import { parseJSON } from './utils/jsonUtils' import { parseJSON } from './utils/jsonUtils'
import { jsonToData, dataToJson, patchData } from './jsonData' import { jsonToData, dataToJson, patchData } from './jsonData'
import ModeButton from './menu/ModeButton'
export default class TextMode extends Component { export default class TextMode extends Component {
// TODO: define propTypes // TODO: define propTypes
@ -25,8 +26,19 @@ export default class TextMode extends Component {
class: 'jsoneditor-compact', class: 'jsoneditor-compact',
title: 'Compact the JSON document', title: 'Compact the JSON document',
onClick: this.handleCompact onClick: this.handleCompact
}) }),
// TODO: implement a button "Fix JSON" // 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
})
]), ]),
h('div', {class: 'jsoneditor-contents'}, [ h('div', {class: 'jsoneditor-contents'}, [

View File

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

View File

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

View File

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

View File

@ -306,6 +306,11 @@ div.jsoneditor-contextmenu.jsoneditor-contextmenu-top {
bottom: 20px; bottom: 20px;
} }
div.jsoneditor-modemenu.jsoneditor-modemenu {
top: 26px;
left: 0;
}
div.jsoneditor-menu-item { div.jsoneditor-menu-item {
line-height: 0; line-height: 0;
font-size: 0; font-size: 0;
@ -500,6 +505,28 @@ button.jsoneditor-type-Array.jsoneditor-selected span.jsoneditor-icon {
background-position: -96px 0; 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 { textarea.jsoneditor-text {
width: 100%; width: 100%;

View File

@ -20,8 +20,6 @@ export default class Menu extends Component {
expanding: null, // menu index of expanding menu item expanding: null, // menu index of expanding menu item
collapsing: null // menu index of collapsing 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') { if (item.type === 'separator') {
return h('div', {class: 'jsoneditor-menu-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 {{ * @typedef {{
* mode: 'tree' | 'text', * mode: 'tree' | 'text',
* modes: string[],
* indentation: number | string, * indentation: number | string,
* onChange: function (patch: JSONPatch, revert: JSONPatch), * onChange: function (patch: JSONPatch, revert: JSONPatch),
* onError: function (err: Error) * onError: function (err: Error)

View File

@ -110,3 +110,12 @@ export function findUniqueName (name, invalidNames) {
return validName 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()
}