Implemented switch mode menu
This commit is contained in:
parent
88aed193c6
commit
873d5f8ae2
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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'}, [
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
13
src/index.js
13
src/index.js
|
@ -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)
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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'})
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -32,6 +32,7 @@
|
|||
*
|
||||
* @typedef {{
|
||||
* mode: 'tree' | 'text',
|
||||
* modes: string[],
|
||||
* indentation: number | string,
|
||||
* onChange: function (patch: JSONPatch, revert: JSONPatch),
|
||||
* onError: function (err: Error)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue