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) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'}, [
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
13
src/index.js
13
src/index.js
|
@ -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)
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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'})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {{
|
* @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)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue