mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-22 07:08:10 +00:00
replace dropdown with popper.js-based implementation
This commit is contained in:
parent
f69c91cb36
commit
a034b7c2c1
@ -12,7 +12,6 @@
|
||||
"classnames": "^2.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mock-xmlhttprequest": "^1.1.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-codemirror": "^1.0.0",
|
||||
|
@ -19,3 +19,4 @@ html {
|
||||
@import (less) "codemirror.less";
|
||||
@import (less) "contentview.less";
|
||||
@import (less) "modal.less";
|
||||
@import (less) "dropdown.less";
|
||||
|
@ -1,4 +1,3 @@
|
||||
hr.divider {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
.dropdown-menu > li > a {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
@ -17,14 +17,14 @@
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
thead {
|
||||
thead tr {
|
||||
background-color: #F2F2F2;
|
||||
border-bottom: solid #bebebe 1px;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: normal;
|
||||
box-shadow: 0 1px 0 #a6a6a6;
|
||||
position: relative !important;
|
||||
padding-left: 1px;
|
||||
.user-select(none);
|
||||
@ -155,12 +155,15 @@
|
||||
.col-quickactions {
|
||||
width: 0;
|
||||
direction: rtl;
|
||||
* {
|
||||
direction: ltr;
|
||||
}
|
||||
overflow: hidden;
|
||||
background-color: inherit;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
tr:hover .col-quickactions {
|
||||
tr:hover .col-quickactions, .col-quickactions.hover {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@ -170,72 +173,24 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
> a {
|
||||
margin-right: 2px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 5%);
|
||||
&:hover, &.open {
|
||||
background-color: rgba(0, 0, 0, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col-quickactions .fa-ellipsis-h {
|
||||
transform: translate(-3px, 3px);
|
||||
transform: translate(0, 3px);
|
||||
}
|
||||
|
||||
.col-quickactions .fa-play {
|
||||
transform: translate(-1px, 2px);
|
||||
transform: translate(1px, 2px);
|
||||
}
|
||||
|
||||
.dropdown-submenu {
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.dropdown-submenu>.dropdown-menu {
|
||||
top:0;
|
||||
left:100%;
|
||||
margin-top:-6px;
|
||||
margin-left:-1px;
|
||||
-webkit-border-radius:0 6px 6px 6px;
|
||||
-moz-border-radius:0 6px 6px 6px;
|
||||
border-radius:0 6px 6px 6px;
|
||||
}
|
||||
|
||||
.dropdown-submenu:hover>.dropdown-menu {
|
||||
display:block;
|
||||
}
|
||||
|
||||
.dropdown-submenu>a:after {
|
||||
display:block;
|
||||
content:" ";
|
||||
float:right;
|
||||
width:0;
|
||||
height:0;
|
||||
border-color:transparent;
|
||||
border-style:solid;
|
||||
border-width:5px 0 5px 5px;
|
||||
border-left-color:#cccccc;
|
||||
margin-top:5px;
|
||||
margin-right:-10px;
|
||||
}
|
||||
|
||||
.dropdown-submenu:hover>a:after {
|
||||
border-left-color:#ffffff;
|
||||
}
|
||||
|
||||
.dropdown-submenu.pull-left {
|
||||
float:none;
|
||||
}
|
||||
|
||||
.dropdown-submenu.pull-left>.dropdown-menu {
|
||||
left:-100%;
|
||||
margin-left:10px;
|
||||
-webkit-border-radius:6px 0 6px 6px;
|
||||
-moz-border-radius:6px 0 6px 6px;
|
||||
border-radius:6px 0 6px 6px;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
border-bottom: solid @separator-color 1px;
|
||||
|
||||
a {
|
||||
> a {
|
||||
display: inline-block;
|
||||
border: solid transparent 1px;
|
||||
text-decoration: none;
|
||||
@ -30,20 +30,20 @@
|
||||
}
|
||||
|
||||
.nav-tabs-lg {
|
||||
a {
|
||||
> a {
|
||||
padding: 3px 14px;
|
||||
margin: 0 2px -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs-sm {
|
||||
a {
|
||||
> a {
|
||||
padding: 0px 7px;
|
||||
margin: 2px 2px -1px;
|
||||
}
|
||||
a.nav-action {
|
||||
> a.nav-action {
|
||||
float: right;
|
||||
padding: 0;
|
||||
margin: 1px 0 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,27 +3,18 @@ import renderer from 'react-test-renderer'
|
||||
import Dropdown, { Divider } from '../../../components/common/Dropdown'
|
||||
|
||||
describe('Dropdown Component', () => {
|
||||
let dropup = renderer.create(<Dropdown dropup btnClass="foo">
|
||||
<a href="#">1</a>
|
||||
<Divider/>
|
||||
<a href="#">2</a>
|
||||
</Dropdown>),
|
||||
dropdown = renderer.create(<Dropdown btnClass="foo">
|
||||
let dropdown = renderer.create(<Dropdown text="open me">
|
||||
<a href="#">1</a>
|
||||
<a href="#">2</a>
|
||||
</Dropdown>)
|
||||
|
||||
it('should render correctly', () => {
|
||||
let tree = dropup.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
tree = dropdown.toJSON()
|
||||
let tree = dropdown.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle open/close action', () => {
|
||||
document.body.addEventListener('click', ()=>{})
|
||||
let tree = dropup.toJSON(),
|
||||
let tree = dropdown.toJSON(),
|
||||
e = { preventDefault: jest.fn(), stopPropagation: jest.fn() }
|
||||
tree.children[0].props.onClick(e)
|
||||
expect(tree).toMatchSnapshot()
|
||||
@ -31,6 +22,9 @@ describe('Dropdown Component', () => {
|
||||
// click action when the state is open
|
||||
tree.children[0].props.onClick(e)
|
||||
|
||||
// open again
|
||||
tree.children[0].props.onClick(e)
|
||||
|
||||
// close
|
||||
document.body.click()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { setContentView } from '../../ducks/ui/flow';
|
||||
import Dropdown from '../common/Dropdown'
|
||||
import {connect} from 'react-redux'
|
||||
import {setContentView} from '../../ducks/ui/flow';
|
||||
import Dropdown, {MenuItem} from '../common/Dropdown'
|
||||
|
||||
|
||||
ViewSelector.propTypes = {
|
||||
@ -11,22 +11,24 @@ ViewSelector.propTypes = {
|
||||
setContentView: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export function ViewSelector ({contentViews, activeView, setContentView}){
|
||||
let inner = <span> <b>View:</b> {activeView.toLowerCase()} <span className="caret"/> </span>
|
||||
export function ViewSelector({contentViews, activeView, setContentView}) {
|
||||
let inner = <span><b>View:</b> {activeView.toLowerCase()} <span className="caret"/></span>
|
||||
|
||||
return (
|
||||
<Dropdown dropup className="pull-left" btnClass="btn btn-default btn-xs" text={inner}>
|
||||
<Dropdown
|
||||
text={inner}
|
||||
className="btn btn-default btn-xs pull-left"
|
||||
options={{placement:"top-start"}}>
|
||||
{contentViews.map(name =>
|
||||
<a href="#" key={name} onClick={e => {e.preventDefault(); setContentView(name)}}>
|
||||
<MenuItem key={name} onClick={() => setContentView(name)}>
|
||||
{name.toLowerCase().replace('_', ' ')}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect (
|
||||
export default connect(
|
||||
state => ({
|
||||
contentViews: state.settings.contentViews || [],
|
||||
activeView: state.ui.flow.contentView,
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {useDispatch} from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import {RequestUtils, ResponseUtils} from '../../flow/utils.js'
|
||||
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils.js'
|
||||
import * as flowActions from "../../ducks/flows";
|
||||
import HoverMenu from "./HoverMenu";
|
||||
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
|
||||
|
||||
export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"]
|
||||
|
||||
@ -167,22 +167,29 @@ TimeStampColumn.headerName = 'TimeStamp'
|
||||
export function QuickActionsColumn({flow, selected}) {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
function resume(e) {
|
||||
dispatch(flowActions.resume(flow))
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let [open, setOpen] = useState(false)
|
||||
|
||||
let intercept = null;
|
||||
if (flow.intercepted) {
|
||||
intercept = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
||||
<i className="fa fa-fw fa-play text-success"/>
|
||||
</a>;
|
||||
}
|
||||
|
||||
return (
|
||||
<td className="col-quickactions">
|
||||
<td className={classnames("col-quickactions", {hover: open})} onClick={(e) => e.stopPropagation()}>
|
||||
<div>
|
||||
{selected ?
|
||||
<div className="quickaction"><HoverMenu /></div>
|
||||
: null}
|
||||
{flow.intercepted
|
||||
? <div className="quickaction" onClick={resume}><i className="fa fa-fw fa-play text-success"/></div>
|
||||
: null}
|
||||
{intercept}
|
||||
<Dropdown text={<i className="fa fa-fw fa-ellipsis-h"/>} className="quickaction" onOpen={setOpen} options={{placement: "bottom-end"}}>
|
||||
<MenuItem onClick={() => alert("Foo!")}>Foo</MenuItem>
|
||||
<SubMenu title="Bar">
|
||||
<MenuItem>Qux</MenuItem>
|
||||
<MenuItem>Quux</MenuItem>
|
||||
</SubMenu>
|
||||
<MenuItem>Baz</MenuItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
import React, { Component, useCallback } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { update as updateSettings } from "../../ducks/settings"
|
||||
import Dropdown from '../common/Dropdown'
|
||||
import DropdownSubMenu from '../common/DropdownSubMenu'
|
||||
|
||||
let SubMenu = ({flow, intercept, updateSettings}) => {
|
||||
const onClick = useCallback((e, example) => {
|
||||
e.preventDefault();
|
||||
if (intercept && intercept.includes(example)) {
|
||||
return
|
||||
}
|
||||
if (!intercept) {
|
||||
intercept = example
|
||||
} else {
|
||||
intercept = `${intercept} | ${example}`
|
||||
}
|
||||
updateSettings({ intercept })
|
||||
}, [intercept])
|
||||
|
||||
return (
|
||||
<DropdownSubMenu text="Intercept requests like this">
|
||||
<a href="#" onClick={(e) =>{
|
||||
onClick(e, flow.request.host)
|
||||
}}>
|
||||
<i className="fa fa-fw fa-plus"></i>
|
||||
Intercept {flow.request.host}
|
||||
</a>
|
||||
{ flow.request.path != "/" ? <a href="#" onClick={(e) =>{
|
||||
onClick(e, flow.request.host + flow.request.path)
|
||||
}}>
|
||||
<i className="fa fa-fw fa-plus"></i>
|
||||
Intercept {flow.request.host + flow.request.path}
|
||||
</a> : null }
|
||||
<a href="#" onClick={(e) =>{
|
||||
onClick(e, `~m POST & ${flow.request.host}`)
|
||||
}}>
|
||||
<i className="fa fa-fw fa-plus"></i>
|
||||
Intercept all POST requests from this host
|
||||
</a>
|
||||
</DropdownSubMenu>
|
||||
)
|
||||
}
|
||||
|
||||
SubMenu = connect(
|
||||
state => ({
|
||||
flow: state.flows.byId[state.flows.selected[0]],
|
||||
intercept: state.settings.intercept,
|
||||
}),
|
||||
{
|
||||
updateSettings,
|
||||
}
|
||||
)(SubMenu)
|
||||
|
||||
class HoverMenu extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { flow } = this.props
|
||||
if (!flow) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Dropdown className="pull-right" btnClass="special" icon="fa fa-fw fa-ellipsis-h" submenu={<SubMenu />}>
|
||||
<a href="#" onClick={(e) =>{
|
||||
e.preventDefault()
|
||||
}}>
|
||||
<i className="fa fa-fw fa-plus"></i>
|
||||
Copy as Curl
|
||||
</a>
|
||||
<a href="#" onClick={(e) =>{
|
||||
e.preventDefault()
|
||||
}}>
|
||||
<i className="fa fa-fw fa-plus"></i>
|
||||
Download HAR
|
||||
</a>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
flow: state.flows.byId[state.flows.selected[0]],
|
||||
}),
|
||||
null
|
||||
)(HoverMenu)
|
@ -1,10 +1,9 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {connect} from 'react-redux'
|
||||
import FileChooser from '../common/FileChooser'
|
||||
import Dropdown, {Divider} from '../common/Dropdown'
|
||||
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
|
||||
import * as flowsActions from '../../ducks/flows'
|
||||
import * as modalActions from '../../ducks/ui/modal'
|
||||
import HideInStatic from "../common/HideInStatic";
|
||||
|
||||
FileMenu.propTypes = {
|
||||
@ -19,29 +18,36 @@ FileMenu.onNewClick = (e, clearFlows) => {
|
||||
clearFlows()
|
||||
}
|
||||
|
||||
export function FileMenu ({clearFlows, loadFlows, saveFlows}) {
|
||||
return (
|
||||
<Dropdown className="pull-left" btnClass="special" text="mitmproxy">
|
||||
<a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}>
|
||||
<i className="fa fa-fw fa-trash"></i>
|
||||
export function FileMenu({clearFlows, loadFlows, saveFlows}) {
|
||||
return (
|
||||
<Dropdown className="pull-left special" text="mitmproxy" options={{"placement": "bottom-start"}}>
|
||||
<MenuItem onClick={e => FileMenu.onNewClick(e, clearFlows)}>
|
||||
<i className="fa fa-fw fa-trash"/>
|
||||
Clear All
|
||||
</a>
|
||||
<FileChooser
|
||||
icon="fa-folder-open"
|
||||
text=" Open..."
|
||||
onOpenFile={file => loadFlows(file)}
|
||||
/>
|
||||
<a href="#" onClick={e =>{ e.preventDefault(); saveFlows();}}>
|
||||
<i className="fa fa-fw fa-floppy-o"></i>
|
||||
</MenuItem>
|
||||
<li>
|
||||
<FileChooser
|
||||
icon="fa-folder-open"
|
||||
text=" Open..."
|
||||
onOpenFile={file => loadFlows(file)}
|
||||
/>
|
||||
</li>
|
||||
<MenuItem onClick={e => {
|
||||
e.preventDefault();
|
||||
saveFlows();
|
||||
}}>
|
||||
<i className="fa fa-fw fa-floppy-o"/>
|
||||
Save...
|
||||
</a>
|
||||
</MenuItem>
|
||||
|
||||
<HideInStatic>
|
||||
<Divider/>
|
||||
<a href="http://mitm.it/" target="_blank">
|
||||
<i className="fa fa-fw fa-external-link"></i>
|
||||
Install Certificates...
|
||||
</a>
|
||||
<Divider/>
|
||||
<li>
|
||||
<a href="http://mitm.it/" target="_blank">
|
||||
<i className="fa fa-fw fa-external-link"/>
|
||||
Install Certificates...
|
||||
</a>
|
||||
</li>
|
||||
</HideInStatic>
|
||||
</Dropdown>
|
||||
)
|
||||
|
@ -1,57 +0,0 @@
|
||||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export const Divider = () => <hr className="divider"/>
|
||||
|
||||
export default class Dropdown extends Component {
|
||||
|
||||
static propTypes = {
|
||||
dropup: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
btnClass: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
dropup: false
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {open: false}
|
||||
this.close = this.close.bind(this)
|
||||
this.open = this.open.bind(this)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.setState({open: false})
|
||||
document.removeEventListener('click', this.close)
|
||||
}
|
||||
|
||||
open(e) {
|
||||
e.preventDefault()
|
||||
if (this.state.open) {
|
||||
return
|
||||
}
|
||||
e.stopPropagation();
|
||||
this.setState({open: !this.state.open})
|
||||
document.addEventListener('click', this.close)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {dropup, className, btnClass, text, icon, children, submenu} = this.props
|
||||
return (
|
||||
<div className={classnames((dropup ? 'dropup' : 'dropdown'), className, {open: this.state.open})}>
|
||||
<a href='#' className={btnClass}
|
||||
onClick={this.open}>
|
||||
{icon ? <i className={icon}/> : null}
|
||||
{text}
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
{children.map((item, i) => <li key={i}> {item} </li>)}
|
||||
{submenu}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
114
web/src/js/components/common/Dropdown.tsx
Normal file
114
web/src/js/components/common/Dropdown.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {usePopper} from "react-popper";
|
||||
import * as PopperJS from "@popperjs/core";
|
||||
import classnames from "classnames";
|
||||
|
||||
export const Divider = () => <li role="separator" className="divider"/>;
|
||||
|
||||
|
||||
type MenuItemProps = {
|
||||
onClick: () => void,
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function MenuItem({onClick, children, ...attrs}: MenuItemProps) {
|
||||
|
||||
const click = (e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
|
||||
return <li>
|
||||
<a
|
||||
href="#"
|
||||
onClick={click}
|
||||
{...attrs}>
|
||||
{children}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
type SubMenuProps = {
|
||||
title: string,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
export function SubMenu({title, children}: SubMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [referenceElement, setReferenceElement] = useState(null);
|
||||
const [popperElement, setPopperElement] = useState(null);
|
||||
const {styles, attributes} = usePopper(referenceElement, popperElement, {placement: "right-start"});
|
||||
|
||||
let submenu = null;
|
||||
if (open) {
|
||||
submenu = <ul className="dropdown-menu show" ref={setPopperElement}
|
||||
style={styles.popper} {...attributes.popper}>{children}</ul>;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={setReferenceElement}
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
>
|
||||
<a><i className="fa fa-caret-right pull-right" aria-hidden="true"/> {title}</a>
|
||||
{submenu}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
type DropdownProps = {
|
||||
text: React.ReactNode,
|
||||
children: React.ReactNode,
|
||||
options?: Partial<PopperJS.Options>,
|
||||
className?: string,
|
||||
onOpen?: (boolean) => void
|
||||
}
|
||||
const Dropdown = ({text, children, options, className, onOpen, ...attrs}: DropdownProps) => {
|
||||
|
||||
const [refElement, setRefElement] = useState(null);
|
||||
const [open, _setOpen] = useState(false);
|
||||
const [popperElement, setPopperElement] = useState(null);
|
||||
const {styles, attributes} = usePopper(refElement, popperElement, {...options});
|
||||
|
||||
let setOpen = (b: boolean) => {
|
||||
_setOpen(b);
|
||||
onOpen && onOpen(b);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open)
|
||||
return
|
||||
document.addEventListener("click", () => {
|
||||
// a bit tricky: we need to wait here for a bit so that we don't double-toggle
|
||||
// when clicking the dropdown button.
|
||||
setTimeout(() => setOpen(false));
|
||||
}, {capture: true, once: true});
|
||||
}, [open]);
|
||||
|
||||
let contents;
|
||||
if (open) {
|
||||
contents = <ul
|
||||
className="dropdown-menu show"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}>
|
||||
{children}
|
||||
</ul>
|
||||
} else {
|
||||
contents = null;
|
||||
}
|
||||
|
||||
return <>
|
||||
<a href="#"
|
||||
ref={setRefElement}
|
||||
className={classnames(className, {"open": open})}
|
||||
onClick={() => setOpen(true)}
|
||||
{...attrs}>
|
||||
{text}
|
||||
|
||||
</a>
|
||||
{contents}
|
||||
</>;
|
||||
}
|
||||
export default Dropdown;
|
@ -1,73 +0,0 @@
|
||||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Manager, Reference, Popper } from 'react-popper';
|
||||
import classnames from 'classnames'
|
||||
|
||||
export const Divider = () => <hr className="divider"/>
|
||||
|
||||
export default class DropdownSubMenu extends Component {
|
||||
|
||||
static propTypes = {
|
||||
dropup: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
dropup: false
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {open: false}
|
||||
this.close = this.close.bind(this)
|
||||
this.open = this.open.bind(this)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.setState({open: false})
|
||||
document.removeEventListener('click', this.close)
|
||||
}
|
||||
|
||||
open(e) {
|
||||
e.preventDefault()
|
||||
if (this.state.open) {
|
||||
return
|
||||
}
|
||||
e.stopPropagation();
|
||||
this.setState({open: !this.state.open})
|
||||
document.addEventListener('click', this.close)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {text, children} = this.props
|
||||
|
||||
return (
|
||||
<li className="dropdown-submenu">
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<a tabIndex="-1" ref={ref} href="#">{text}</a>
|
||||
)}
|
||||
</Reference>
|
||||
<Popper
|
||||
placement="bottom-end"
|
||||
modifiers={[
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 30],
|
||||
}
|
||||
},
|
||||
]}
|
||||
>
|
||||
{({ ref, style, placement, arrowProps }) => (
|
||||
<ul ref={ref} style={style} data-placement={placement} className="dropdown-menu pull-left">
|
||||
{children.map((item, i) => <li key={i}> {item} </li>)}
|
||||
</ul>
|
||||
)}
|
||||
</Popper>
|
||||
</Manager>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
6
web/tsconfig.json
Normal file
6
web/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
@ -1208,6 +1208,11 @@
|
||||
"@types/yargs" "^16.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@popperjs/core@^2.9.2":
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
||||
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
|
||||
@ -5927,6 +5932,11 @@ react-dom@^17.0.2:
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-fast-compare@^3.0.1:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||
|
||||
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
@ -5937,6 +5947,14 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-popper@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96"
|
||||
integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-redux@^7.2.4:
|
||||
version "7.2.4"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
|
||||
@ -7321,6 +7339,13 @@ walker@^1.0.7, walker@~1.0.5:
|
||||
dependencies:
|
||||
makeerror "1.0.x"
|
||||
|
||||
warning@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
webidl-conversions@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
|
||||
|
Loading…
Reference in New Issue
Block a user