Fixes in life cycle of ContextMenu. Adjust top/bottom orientation
This commit is contained in:
parent
a9f0fe07c1
commit
c3c836fa89
|
@ -1,28 +1,41 @@
|
||||||
import { h, Component } from 'preact'
|
import { h, render, Component } from 'preact'
|
||||||
|
|
||||||
|
export let CONTEXT_MENU_HEIGHT = 240
|
||||||
|
|
||||||
export default class ContextMenu extends Component {
|
export default class ContextMenu extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
// determine orientation
|
||||||
|
const anchorRect = this.props.anchor.getBoundingClientRect()
|
||||||
|
const rootRect = this.props.root.getBoundingClientRect()
|
||||||
|
const orientation = (rootRect.bottom - anchorRect.bottom < CONTEXT_MENU_HEIGHT &&
|
||||||
|
anchorRect.top - rootRect.top > CONTEXT_MENU_HEIGHT)
|
||||||
|
? 'top'
|
||||||
|
: 'bottom'
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
orientation,
|
||||||
expanded: null, // menu index of expanded menu item
|
expanded: null, // menu index of expanded menu item
|
||||||
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.onUpdate = [] // handlers to be executed after component update
|
|
||||||
|
|
||||||
this.renderMenuItem = this.renderMenuItem.bind(this)
|
this.renderMenuItem = this.renderMenuItem.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
if (!this.props.items) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: create a non-visible button to set the focus to the menu
|
// TODO: create a non-visible button to set the focus to the menu
|
||||||
// TODO: implement (customizable) quick keys
|
// TODO: implement (customizable) quick keys
|
||||||
|
|
||||||
// TODO: render the context menu on top when there is no space below the node
|
const className = 'jsoneditor-contextmenu ' +
|
||||||
|
((this.state.orientation === 'top') ? 'jsoneditor-contextmenu-top' : 'jsoneditor-contextmenu-bottom')
|
||||||
|
|
||||||
return h('div', {class: 'jsoneditor-contextmenu'},
|
return h('div', {class: className},
|
||||||
this.props.items.map(this.renderMenuItem)
|
this.props.items.map(this.renderMenuItem)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -47,7 +60,6 @@ export default class ContextMenu extends Component {
|
||||||
}
|
}
|
||||||
else if (item.submenu) {
|
else if (item.submenu) {
|
||||||
// button expands the submenu
|
// button expands the submenu
|
||||||
|
|
||||||
return h('div', {class: 'jsoneditor-menu-item'}, [
|
return h('div', {class: 'jsoneditor-menu-item'}, [
|
||||||
h('button', {class: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick: this.createExpandHandler(index) }, [
|
h('button', {class: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick: this.createExpandHandler(index) }, [
|
||||||
h('span', {class: 'jsoneditor-icon'}),
|
h('span', {class: 'jsoneditor-icon'}),
|
||||||
|
@ -73,7 +85,7 @@ export default class ContextMenu extends Component {
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
*/
|
*/
|
||||||
renderSubMenu (submenu, index) {
|
renderSubMenu (submenu, index) {
|
||||||
const expanding = this.state.expanding === index
|
const expanded = this.state.expanded === index
|
||||||
const collapsing = this.state.collapsing === index
|
const collapsing = this.state.collapsing === index
|
||||||
|
|
||||||
const contents = submenu.map(item => {
|
const contents = submenu.map(item => {
|
||||||
|
@ -86,7 +98,7 @@ export default class ContextMenu extends Component {
|
||||||
})
|
})
|
||||||
|
|
||||||
const className = 'jsoneditor-submenu ' +
|
const className = 'jsoneditor-submenu ' +
|
||||||
(expanding ? ' jsoneditor-expanding' : '') +
|
(expanded ? ' jsoneditor-expanded' : '') +
|
||||||
(collapsing ? ' jsoneditor-collapsing' : '')
|
(collapsing ? ' jsoneditor-collapsing' : '')
|
||||||
|
|
||||||
return h('div', {class: className}, contents)
|
return h('div', {class: className}, contents)
|
||||||
|
@ -100,16 +112,9 @@ export default class ContextMenu extends Component {
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: (prev === index) ? null : index,
|
expanded: (prev === index) ? null : index,
|
||||||
expanding: null,
|
|
||||||
collapsing: prev
|
collapsing: prev
|
||||||
})
|
})
|
||||||
|
|
||||||
this.onUpdate.push(() => {
|
|
||||||
this.setState({
|
|
||||||
expanding: this.state.expanded
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// timeout after unit is collapsed
|
// timeout after unit is collapsed
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (prev === this.state.collapsing) {
|
if (prev === this.state.collapsing) {
|
||||||
|
@ -120,10 +125,4 @@ export default class ContextMenu extends Component {
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this.onUpdate.forEach(handler => handler())
|
|
||||||
this.onUpdate = []
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ export default class JSONNode extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
// TODO: create a function bindMethods(this)
|
||||||
this.handleChangeProperty = this.handleChangeProperty.bind(this)
|
this.handleChangeProperty = this.handleChangeProperty.bind(this)
|
||||||
this.handleChangeValue = this.handleChangeValue.bind(this)
|
this.handleChangeValue = this.handleChangeValue.bind(this)
|
||||||
this.handleClickValue = this.handleClickValue.bind(this)
|
this.handleClickValue = this.handleClickValue.bind(this)
|
||||||
|
@ -159,16 +160,18 @@ export default class JSONNode extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContextMenuButton () {
|
renderContextMenuButton () {
|
||||||
const visible = this.props.data.menu === true
|
const className = 'jsoneditor-button jsoneditor-contextmenu' +
|
||||||
|
(this.props.data.contextMenu ? ' jsoneditor-visible' : '')
|
||||||
|
|
||||||
const className = 'jsoneditor-button jsoneditor-contextmenu' + (visible ? ' jsoneditor-visible' : '')
|
|
||||||
return h('div', {class: 'jsoneditor-button-container'},
|
return h('div', {class: 'jsoneditor-button-container'},
|
||||||
visible ? this.renderContextMenu() : null,
|
this.props.data.contextMenu
|
||||||
|
? this.renderContextMenu(this.props.data.contextMenu)
|
||||||
|
: null,
|
||||||
h('button', {class: className, onClick: this.handleContextMenu})
|
h('button', {class: className, onClick: this.handleContextMenu})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContextMenu () {
|
renderContextMenu ({anchor, root}) {
|
||||||
const hasParent = this.props.data.path !== ''
|
const hasParent = this.props.data.path !== ''
|
||||||
const type = this.props.data.type
|
const type = this.props.data.type
|
||||||
const items = [] // array with menu items
|
const items = [] // array with menu items
|
||||||
|
@ -333,10 +336,11 @@ export default class JSONNode extends Component {
|
||||||
|
|
||||||
// TODO: implement a hook to adjust the context menu
|
// TODO: implement a hook to adjust the context menu
|
||||||
|
|
||||||
return h(ContextMenu, {items})
|
return h(ContextMenu, {anchor, root, items})
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
// WARNING: we suppose that JSONNode is stateless, we don't check changes in the state, only in props
|
||||||
return Object.keys(nextProps).some(prop => this.props[prop] !== nextProps[prop])
|
return Object.keys(nextProps).some(prop => this.props[prop] !== nextProps[prop])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,20 +364,20 @@ export default class JSONNode extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeValue (event) {
|
handleChangeValue (event) {
|
||||||
const value = this._getValueFromEvent(event)
|
const value = JSONNode._getValueFromEvent(event)
|
||||||
|
|
||||||
this.props.events.onChangeValue(this.props.data.path, value)
|
this.props.events.onChangeValue(this.props.data.path, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickValue (event) {
|
handleClickValue (event) {
|
||||||
if (event.ctrlKey && event.button === 0) { // Ctrl+Left click
|
if (event.ctrlKey && event.button === 0) { // Ctrl+Left click
|
||||||
this._openLinkIfUrl(event)
|
JSONNode._openLinkIfUrl(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDownValue (event) {
|
handleKeyDownValue (event) {
|
||||||
if (event.ctrlKey && event.which === 13) { // Ctrl+Enter
|
if (event.ctrlKey && event.which === 13) { // Ctrl+Enter
|
||||||
this._openLinkIfUrl(event)
|
JSONNode._openLinkIfUrl(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,16 +388,25 @@ export default class JSONNode extends Component {
|
||||||
handleContextMenu (event) {
|
handleContextMenu (event) {
|
||||||
event.stopPropagation() // stop propagation, because else Main.js will hide the context menu again
|
event.stopPropagation() // stop propagation, because else Main.js will hide the context menu again
|
||||||
|
|
||||||
// toggle visibility of the context menu
|
if (this.props.data.contextMenu) {
|
||||||
const path = this.props.data.menu === true
|
this.props.events.hideContextMenu()
|
||||||
? null
|
}
|
||||||
: this.props.data.path
|
else {
|
||||||
|
this.props.events.showContextMenu({
|
||||||
this.props.events.onContextMenu(path)
|
path: this.props.data.path,
|
||||||
|
anchor: event.target,
|
||||||
|
root: JSONNode._findRootElement(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_openLinkIfUrl (event) {
|
/**
|
||||||
const value = this._getValueFromEvent(event)
|
* When this JSONNode holds an URL as value, open this URL in a new browser tab
|
||||||
|
* @param event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static _openLinkIfUrl (event) {
|
||||||
|
const value = JSONNode._getValueFromEvent(event)
|
||||||
|
|
||||||
if (isUrl(value)) {
|
if (isUrl(value)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -403,7 +416,33 @@ export default class JSONNode extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getValueFromEvent (event) {
|
static _getValueFromEvent (event) {
|
||||||
return stringConvert(unescapeHTML(getInnerText(event.target)))
|
return stringConvert(unescapeHTML(getInnerText(event.target)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the root DOM element of the JSONEditor
|
||||||
|
* Search is done based on the CSS class 'jsoneditor'
|
||||||
|
* @param event
|
||||||
|
* @return {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static _findRootElement (event) {
|
||||||
|
function isEditorElement (elem) {
|
||||||
|
return elem.className.split(' ').indexOf('jsoneditor') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem = event.target
|
||||||
|
while (elem) {
|
||||||
|
if (isEditorElement(elem)) {
|
||||||
|
return elem
|
||||||
|
}
|
||||||
|
|
||||||
|
elem = elem.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
38
src/Main.js
38
src/Main.js
|
@ -9,6 +9,13 @@ export default class Main extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
// TODO: create a function bindMethods(this)
|
||||||
|
this.handleChangeProperty = this.handleChangeProperty.bind(this)
|
||||||
|
this.handleChangeValue = this.handleChangeValue.bind(this)
|
||||||
|
this.handleExpand = this.handleExpand.bind(this)
|
||||||
|
this.handleShowContextMenu = this.handleShowContextMenu.bind(this)
|
||||||
|
this.handleHideContextMenu = this.handleHideContextMenu.bind(this)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
options: Object.assign({
|
options: Object.assign({
|
||||||
name: null,
|
name: null,
|
||||||
|
@ -23,19 +30,18 @@ export default class Main extends Component {
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
onChangeProperty: this.handleChangeProperty.bind(this),
|
onChangeProperty: this.handleChangeProperty,
|
||||||
onChangeValue: this.handleChangeValue.bind(this),
|
onChangeValue: this.handleChangeValue,
|
||||||
onExpand: this.handleExpand.bind(this),
|
onExpand: this.handleExpand,
|
||||||
onContextMenu: this.handleContextMenu.bind(this)
|
showContextMenu: this.handleShowContextMenu,
|
||||||
|
hideContextMenu: this.handleHideContextMenu
|
||||||
},
|
},
|
||||||
|
|
||||||
/** @type {string | null} */
|
/** @type {string | null} */
|
||||||
menu: null, // json pointer to the node having menu visible
|
contextMenuPath: null, // json pointer to the node having menu visible
|
||||||
|
|
||||||
search: null
|
search: null
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleHideContextMenu = this.handleHideContextMenu.bind(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -84,32 +90,34 @@ export default class Main extends Component {
|
||||||
/**
|
/**
|
||||||
* Set ContextMenu to a json pointer, or hide the context menu by passing null
|
* Set ContextMenu to a json pointer, or hide the context menu by passing null
|
||||||
* @param {string | null} path
|
* @param {string | null} path
|
||||||
|
* @param {Element} anchor
|
||||||
|
* @param {Element} root
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
handleContextMenu(path) {
|
handleShowContextMenu({path, anchor, root}) {
|
||||||
let data = this.state.data
|
let data = this.state.data
|
||||||
|
|
||||||
// hide previous context menu (if any)
|
// hide previous context menu (if any)
|
||||||
if (this.state.menu !== null) {
|
if (this.state.contextMenuPath !== null) {
|
||||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(this.state.menu))
|
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(this.state.contextMenuPath))
|
||||||
data = setIn(data, modelPath.concat('menu'), false)
|
data = setIn(data, modelPath.concat('contextMenu'), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// show new menu
|
// show new menu
|
||||||
if (path !== null) {
|
if (typeof path === 'string') {
|
||||||
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
const modelPath = Main._pathToModelPath(this.state.data, Main._parsePath(path))
|
||||||
data = setIn(data, modelPath.concat('menu'), true)
|
data = setIn(data, modelPath.concat('contextMenu'), {anchor, root})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
menu: path, // store path of current menu, just to easily find it next time
|
contextMenuPath: typeof path === 'string' ? path : null, // store path of current menu, just to easily find it next time
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHideContextMenu (event) {
|
handleHideContextMenu (event) {
|
||||||
// FIXME: find a different way to show/hide the context menu. create a single instance in the Main, pass a reference to it into the JSON nodes?
|
// FIXME: find a different way to show/hide the context menu. create a single instance in the Main, pass a reference to it into the JSON nodes?
|
||||||
this.handleContextMenu(null)
|
this.handleShowContextMenu({})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: comment
|
// TODO: comment
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
// create the editor
|
// create the editor
|
||||||
const container = document.getElementById('container');
|
const container = document.getElementById('container');
|
||||||
const options = {
|
const options = {
|
||||||
name: 'myObject'
|
name: 'myObject',
|
||||||
|
expand: function (path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const editor = jsoneditor(container, options);
|
const editor = jsoneditor(container, options);
|
||||||
const json = {
|
const json = {
|
||||||
|
|
|
@ -170,7 +170,7 @@ button.jsoneditor-button.jsoneditor-contextmenu.jsoneditor-visible {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/******************************* Context Menu ******************************/
|
/******************************* Context Menu *********************************/
|
||||||
|
|
||||||
div.jsoneditor-contextmenu {
|
div.jsoneditor-contextmenu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -184,6 +184,11 @@ div.jsoneditor-contextmenu {
|
||||||
box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3);
|
box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.jsoneditor-contextmenu.jsoneditor-contextmenu-top {
|
||||||
|
top: auto;
|
||||||
|
bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
div.jsoneditor-menu-item {
|
div.jsoneditor-menu-item {
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
|
@ -319,7 +324,7 @@ div.jsoneditor-submenu {
|
||||||
inset 0 -10px 10px -10px rgba(128, 128, 128, 0.5)
|
inset 0 -10px 10px -10px rgba(128, 128, 128, 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
div.jsoneditor-submenu.jsoneditor-expanding {
|
div.jsoneditor-submenu.jsoneditor-expanded {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
max-height: 104px; /* 4 * 24px + 2 * 5px */
|
max-height: 104px; /* 4 * 24px + 2 * 5px */
|
||||||
/* FIXME: shouldn't rely on max-height equal to 4 items, should be flexible */
|
/* FIXME: shouldn't rely on max-height equal to 4 items, should be flexible */
|
||||||
|
|
|
@ -61,20 +61,19 @@ export function isUrl (text) {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export function stringConvert (str) {
|
export function stringConvert (str) {
|
||||||
const lower = str.toLowerCase()
|
|
||||||
const num = Number(str) // will nicely fail with '123ab'
|
const num = Number(str) // will nicely fail with '123ab'
|
||||||
const numFloat = parseFloat(str) // will nicely fail with ' '
|
const numFloat = parseFloat(str) // will nicely fail with ' '
|
||||||
|
|
||||||
if (str == '') {
|
if (str == '') {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
else if (lower == 'null') {
|
else if (str == 'null') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
else if (lower == 'true') {
|
else if (str == 'true') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
else if (lower == 'false') {
|
else if (str == 'false') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
else if (!isNaN(num) && !isNaN(numFloat)) {
|
else if (!isNaN(num) && !isNaN(numFloat)) {
|
||||||
|
|
Loading…
Reference in New Issue