Refactored ActionMenu and AppendActionMenu
This commit is contained in:
parent
1bcc6382aa
commit
5fc5e302dc
|
@ -1,7 +1,7 @@
|
|||
import { h, Component } from 'preact'
|
||||
|
||||
import ActionMenu from './menu/ActionMenu'
|
||||
import AppendActionMenu from './menu/AppendActionMenu'
|
||||
import ActionButton from './menu/ActionButton'
|
||||
import AppendActionButton from './menu/AppendActionButton'
|
||||
import { escapeHTML, unescapeHTML } from './utils/stringUtils'
|
||||
import { getInnerText } from './utils/domUtils'
|
||||
import { stringConvert, valueType, isUrl } from './utils/typeUtils'
|
||||
|
@ -34,6 +34,7 @@ export default class JSONNode extends Component {
|
|||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
// TODO: remove state
|
||||
this.state = {
|
||||
menu: null, // context menu
|
||||
appendMenu: null, // append context menu (used in placeholder of empty object/array)
|
||||
|
@ -281,52 +282,18 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
|
||||
renderActionMenuButton () {
|
||||
const className = 'jsoneditor-button jsoneditor-contextmenu' +
|
||||
(this.state.menu ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {class: 'jsoneditor-button-container'}, [
|
||||
this.renderActionMenu(),
|
||||
h('button', {class: className, onClick: this.handleContextMenu})
|
||||
])
|
||||
return h(ActionButton, {
|
||||
path: this.getPath(),
|
||||
type: this.props.data.type,
|
||||
events: this.props.events
|
||||
})
|
||||
}
|
||||
|
||||
renderAppendContextMenuButton () {
|
||||
const className = 'jsoneditor-button jsoneditor-contextmenu' +
|
||||
(this.state.appendMenu ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {class: 'jsoneditor-button-container'}, [
|
||||
this.renderAppendContextMenu(),
|
||||
h('button', {class: className, onClick: this.handleAppendContextMenu})
|
||||
])
|
||||
}
|
||||
|
||||
renderActionMenu () {
|
||||
if (this.state.menu) {
|
||||
return h(ActionMenu, {
|
||||
anchor: this.state.menu.anchor,
|
||||
root: this.state.menu.root,
|
||||
path: this.getPath(),
|
||||
type: this.props.data.type,
|
||||
events: this.props.events
|
||||
})
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
renderAppendContextMenu () {
|
||||
if (this.state.appendMenu) {
|
||||
return h(AppendActionMenu, {
|
||||
anchor: this.state.menu.anchor,
|
||||
root: this.state.menu.root,
|
||||
path: this.getPath(),
|
||||
events: this.props.events
|
||||
})
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
return h(AppendActionButton, {
|
||||
path: this.getPath(),
|
||||
events: this.props.events
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
@ -500,7 +467,7 @@ export default class JSONNode extends Component {
|
|||
* @param event
|
||||
* @return {*}
|
||||
*/
|
||||
// TODO: move to TreeMode?
|
||||
// TODO: cleanup
|
||||
static findRootElement (event) {
|
||||
function isEditorElement (elem) {
|
||||
// FIXME: this is a bit tricky. can we use a special attribute or something?
|
||||
|
|
|
@ -49,7 +49,10 @@ export default class TreeMode extends Component {
|
|||
|
||||
render (props, state) {
|
||||
// TODO: make mode tree dynamic
|
||||
return h('div', {class: 'jsoneditor jsoneditor-mode-tree'}, [
|
||||
return h('div', {
|
||||
class: 'jsoneditor jsoneditor-mode-tree',
|
||||
'data-jsoneditor': 'true'
|
||||
}, [
|
||||
this.renderMenu(),
|
||||
|
||||
h('div', {class: 'jsoneditor-contents jsoneditor-tree-contents', onClick: this.handleHideMenus}, [
|
||||
|
|
|
@ -275,21 +275,21 @@ button.jsoneditor-button.jsoneditor-expanded {
|
|||
background-position: -2px -74px;
|
||||
}
|
||||
|
||||
button.jsoneditor-button.jsoneditor-contextmenu {
|
||||
button.jsoneditor-button.jsoneditor-actionmenu {
|
||||
background-position: -50px -74px;
|
||||
}
|
||||
|
||||
button.jsoneditor-button.jsoneditor-contextmenu:hover,
|
||||
button.jsoneditor-button.jsoneditor-contextmenu:focus,
|
||||
button.jsoneditor-button.jsoneditor-contextmenu.jsoneditor-visible {
|
||||
button.jsoneditor-button.jsoneditor-actionmenu:hover,
|
||||
button.jsoneditor-button.jsoneditor-actionmenu:focus,
|
||||
button.jsoneditor-button.jsoneditor-actionmenu.jsoneditor-visible {
|
||||
background-position: -50px -50px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/******************************* Context Menu *********************************/
|
||||
/******************************* Action Menu **********************************/
|
||||
|
||||
div.jsoneditor-contextmenu {
|
||||
div.jsoneditor-actionmenu {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
z-index: 99999;
|
||||
|
@ -301,7 +301,7 @@ div.jsoneditor-contextmenu {
|
|||
box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
|
||||
div.jsoneditor-contextmenu.jsoneditor-contextmenu-top {
|
||||
div.jsoneditor-actionmenu.jsoneditor-actionmenu-top {
|
||||
top: auto;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
@ -521,6 +521,7 @@ div.jsoneditor-modes {
|
|||
height: auto;
|
||||
padding: 2px 6px;
|
||||
border-radius: 0;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { h, Component } from 'preact'
|
||||
import ActionMenu from './ActionMenu'
|
||||
import { findParentNode } from '../utils/domUtils'
|
||||
|
||||
export default class ActionButton extends Component {
|
||||
constructor (props) {
|
||||
super (props)
|
||||
|
||||
this.state = {
|
||||
open: false, // whether the menu is open or not
|
||||
anchor: null,
|
||||
root: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{path, type, events}} props
|
||||
* @param state
|
||||
* @return {*}
|
||||
*/
|
||||
render (props, state) {
|
||||
const className = 'jsoneditor-button jsoneditor-actionmenu' +
|
||||
(this.state.open ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {class: 'jsoneditor-button-container'}, [
|
||||
h(ActionMenu, {
|
||||
...props, // path, type, events
|
||||
...state, // open, anchor, root
|
||||
onRequestClose: this.handleRequestClose
|
||||
}),
|
||||
h('button', {class: className, onClick: this.handleOpen})
|
||||
])
|
||||
}
|
||||
|
||||
handleOpen = (event) => {
|
||||
this.setState({
|
||||
open: true,
|
||||
anchor: event.target,
|
||||
root: findParentNode(event.target, 'data-jsoneditor', 'true')
|
||||
})
|
||||
}
|
||||
|
||||
handleRequestClose = () => {
|
||||
this.setState({open: false})
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import {
|
|||
|
||||
export default class ActionMenu extends Component {
|
||||
/**
|
||||
* @param {{anchor, root, path, type, events}} props
|
||||
* @param {{open, anchor, root, path, type, events, onRequestClose}} props
|
||||
* @param state
|
||||
* @return {JSX.Element}
|
||||
*/
|
||||
|
@ -34,8 +34,7 @@ export default class ActionMenu extends Component {
|
|||
// TODO: implement a hook to adjust the action menu
|
||||
|
||||
return h(Menu, {
|
||||
anchor: props.anchor,
|
||||
root: props.root,
|
||||
...props,
|
||||
items
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { h, Component } from 'preact'
|
||||
import AppendActionMenu from './AppendActionMenu'
|
||||
import { findParentNode } from '../utils/domUtils'
|
||||
|
||||
export default class AppendActionButton extends Component {
|
||||
constructor (props) {
|
||||
super (props)
|
||||
|
||||
this.state = {
|
||||
open: false, // whether the menu is open or not
|
||||
anchor: null,
|
||||
root: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{path, events}} props
|
||||
* @param state
|
||||
* @return {*}
|
||||
*/
|
||||
render (props, state) {
|
||||
const className = 'jsoneditor-button jsoneditor-actionmenu' +
|
||||
(this.state.open ? ' jsoneditor-visible' : '')
|
||||
|
||||
return h('div', {class: 'jsoneditor-button-container'}, [
|
||||
h(AppendActionMenu, {
|
||||
...props, // path, events
|
||||
...state, // open, anchor, root
|
||||
onRequestClose: this.handleRequestClose
|
||||
}),
|
||||
h('button', {class: className, onClick: this.handleOpen})
|
||||
])
|
||||
}
|
||||
|
||||
handleOpen = (event) => {
|
||||
this.setState({
|
||||
open: true,
|
||||
anchor: event.target,
|
||||
root: findParentNode(event.target, 'data-jsoneditor', 'true')
|
||||
})
|
||||
}
|
||||
|
||||
handleRequestClose = () => {
|
||||
this.setState({open: false})
|
||||
}
|
||||
}
|
|
@ -16,8 +16,7 @@ export default class AppendActionMenu extends Component {
|
|||
// TODO: implement a hook to adjust the action menu
|
||||
|
||||
return h(Menu, {
|
||||
anchor: props.anchor,
|
||||
root: props.root,
|
||||
...props,
|
||||
items
|
||||
})
|
||||
}
|
||||
|
|
100
src/menu/Menu.js
100
src/menu/Menu.js
|
@ -1,4 +1,5 @@
|
|||
import { h, Component } from 'preact'
|
||||
import { findParentNode } from '../utils/domUtils'
|
||||
|
||||
export let CONTEXT_MENU_HEIGHT = 240
|
||||
|
||||
|
@ -6,16 +7,7 @@ export default class Menu extends Component {
|
|||
constructor(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 = {
|
||||
orientation,
|
||||
expanded: null, // menu index of expanded menu item
|
||||
expanding: null, // menu index of expanding menu item
|
||||
collapsing: null // menu index of collapsing menu item
|
||||
|
@ -23,22 +15,33 @@ export default class Menu extends Component {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {{items: Array}} props
|
||||
* @param {{open: boolean, items: Array, anchor, root, onRequestClose: function}} props
|
||||
* @param state
|
||||
* @return {*}
|
||||
*/
|
||||
render (props, state) {
|
||||
if (!props.items) {
|
||||
if (!props.open) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 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'
|
||||
|
||||
// TODO: create a non-visible button to set the focus to the menu
|
||||
// TODO: implement (customizable) quick keys
|
||||
|
||||
const className = 'jsoneditor-contextmenu ' +
|
||||
((this.state.orientation === 'top') ? 'jsoneditor-contextmenu-top' : 'jsoneditor-contextmenu-bottom')
|
||||
const className = 'jsoneditor-actionmenu ' +
|
||||
((orientation === 'top') ? 'jsoneditor-actionmenu-top' : 'jsoneditor-actionmenu-bottom')
|
||||
|
||||
return h('div', {class: className},
|
||||
return h('div', {
|
||||
class: className,
|
||||
'data-menu': 'true'
|
||||
},
|
||||
props.items.map(this.renderMenuItem)
|
||||
)
|
||||
}
|
||||
|
@ -49,9 +52,15 @@ export default class Menu extends Component {
|
|||
}
|
||||
|
||||
if (item.click && item.submenu) {
|
||||
// FIXME: don't create functions in the render function
|
||||
const onClick = (event) => {
|
||||
item.click()
|
||||
this.props.onRequestClose()
|
||||
}
|
||||
|
||||
// two buttons: direct click and a small button to expand the submenu
|
||||
return h('div', {class: 'jsoneditor-menu-item'}, [
|
||||
h('button', {class: 'jsoneditor-menu-button jsoneditor-menu-default ' + item.className, title: item.title, onClick: item.click }, [
|
||||
h('button', {class: 'jsoneditor-menu-button jsoneditor-menu-default ' + item.className, title: item.title, onClick }, [
|
||||
h('span', {class: 'jsoneditor-icon'}),
|
||||
h('span', {class: 'jsoneditor-text'}, item.text)
|
||||
]),
|
||||
|
@ -73,9 +82,15 @@ export default class Menu extends Component {
|
|||
])
|
||||
}
|
||||
else {
|
||||
// FIXME: don't create functions in the render function
|
||||
const onClick = (event) => {
|
||||
item.click()
|
||||
this.props.onRequestClose()
|
||||
}
|
||||
|
||||
// just a button (no submenu)
|
||||
return h('div', {class: 'jsoneditor-menu-item'}, [
|
||||
h('button', {class: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick: item.click }, [
|
||||
h('button', {class: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick }, [
|
||||
h('span', {class: 'jsoneditor-icon'}),
|
||||
h('span', {class: 'jsoneditor-text'}, item.text)
|
||||
]),
|
||||
|
@ -92,8 +107,14 @@ export default class Menu extends Component {
|
|||
const collapsing = this.state.collapsing === index
|
||||
|
||||
const contents = submenu.map(item => {
|
||||
// FIXME: don't create functions in the render function
|
||||
const onClick = () => {
|
||||
item.click()
|
||||
this.props.onRequestClose()
|
||||
}
|
||||
|
||||
return h('div', {class: 'jsoneditor-menu-item'}, [
|
||||
h('button', {class: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick: item.click }, [
|
||||
h('button', {class: 'jsoneditor-menu-button ' + item.className, title: item.title, onClick }, [
|
||||
h('span', {class: 'jsoneditor-icon'}),
|
||||
h('span', {class: 'jsoneditor-text'}, item.text)
|
||||
]),
|
||||
|
@ -128,4 +149,49 @@ export default class Menu extends Component {
|
|||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
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 (!findParentNode(event.target, 'data-menu', 'true')) {
|
||||
this.props.onRequestClose()
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', this.handleRequestClose)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
removeRequestCloseListener () {
|
||||
if (this.handleRequestClose) {
|
||||
window.removeEventListener('click', this.handleRequestClose)
|
||||
this.handleRequestClose = null
|
||||
}
|
||||
}
|
||||
|
||||
handleRequestClose = null
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { h, Component } from 'preact'
|
||||
import { toCapital } from '../utils/stringUtils'
|
||||
import { findParentNode } from '../utils/domUtils'
|
||||
|
||||
export default class ModeMenu extends Component {
|
||||
/**
|
||||
* @param {{open, modes, mode, onMode, onError}} props
|
||||
* @param {Obect} state
|
||||
* @param {{open, modes, mode, onMode, onRequestClose, onError}} props
|
||||
* @param {Object} state
|
||||
* @return {JSX.Element}
|
||||
*/
|
||||
render (props, state) {
|
||||
|
@ -17,7 +18,7 @@ export default class ModeMenu extends Component {
|
|||
onClick: () => {
|
||||
try {
|
||||
props.onMode(mode)
|
||||
this.setState({ open: false })
|
||||
props.onRequestClose()
|
||||
}
|
||||
catch (err) {
|
||||
props.onError(err)
|
||||
|
@ -27,8 +28,8 @@ export default class ModeMenu extends Component {
|
|||
})
|
||||
|
||||
return h('div', {
|
||||
class: 'jsoneditor-contextmenu jsoneditor-modemenu',
|
||||
'isnodemenu': 'true',
|
||||
class: 'jsoneditor-actionmenu jsoneditor-modemenu',
|
||||
nodemenu: 'true',
|
||||
}, items)
|
||||
}
|
||||
else {
|
||||
|
@ -63,7 +64,7 @@ export default class ModeMenu extends Component {
|
|||
// the menu will immediately result in requestClose event as well
|
||||
setTimeout(() => {
|
||||
this.handleRequestClose = (event) => {
|
||||
if (!ModeMenu.inNodeMenu(event.target)) {
|
||||
if (!findParentNode(event.target, 'data-menu', 'true')) {
|
||||
this.props.onRequestClose()
|
||||
}
|
||||
}
|
||||
|
@ -79,24 +80,5 @@ export default class ModeMenu extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
|
@ -69,6 +69,31 @@ export function getInnerText (element, buffer) {
|
|||
return ''
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Find the parent node of an element which has an attribute with given value.
|
||||
* Can return the element itself too.
|
||||
* @param {Element} elem
|
||||
* @param {string} attr
|
||||
* @param {string} value
|
||||
* @return {Element | null} Returns the parent element when found,
|
||||
* or null otherwise
|
||||
*/
|
||||
export function findParentNode (elem, attr, value) {
|
||||
let parent = elem
|
||||
|
||||
while (parent && parent.getAttribute) {
|
||||
if (parent.getAttribute(attr) == value) {
|
||||
return parent
|
||||
}
|
||||
parent = parent.parentNode
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the version of Internet Explorer or a -1
|
||||
* (indicating the use of another browser).
|
||||
|
|
Loading…
Reference in New Issue