添加 ContextMenu

This commit is contained in:
liaoxuezhi 2019-11-19 16:35:52 +08:00
parent ee68af9036
commit f031dc7401
10 changed files with 494 additions and 108 deletions

View File

@ -125,7 +125,8 @@ $zindex-sticky: 1100 !default;
$zindex-fixed: 1200 !default;
$zindex-modal: 1300 !default;
$zindex-popover: 1400 !default;
$zindex-tooltip: 1500 !default;
$zindex-contextmenu: 1500 !default;
$zindex-tooltip: 1600 !default;
$zindex-toast: 2000 !default;
$gap-xs: px2rem(5px) !default;

View File

@ -0,0 +1,174 @@
@keyframes contextMenuIn {
from {
opacity: 0;
}
}
@keyframes contextMenuOut {
to {
opacity: 0;
}
}
.#{$ns}ContextMenu {
&-menu {
position: absolute;
z-index: $zindex-contextmenu;
&.in,
&.out {
// opacity: 1;
// transform: translateY(0) scale(1);
animation-duration: 0.35s;
animation-fill-mode: both;
}
&.in {
animation-name: contextMenuIn;
}
&.out {
animation-name: contextMenuOut;
}
display: block;
position: absolute;
margin: 0;
padding: 4px 0 5px;
background: rgba(239, 239, 239, 0.95);
box-shadow: 0px 4px 9px rgba(0, 0, 0, 0.34);
border-radius: 7px;
color: rgba(0, 0, 0, 0.75);
font-family: -apple-system, Lucida Grande;
font-size: 14px;
line-height: 15px;
&::before {
display: block;
position: absolute;
content: '';
top: -1px;
left: -1px;
bottom: -1px;
right: -1px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.125);
z-index: -1;
}
}
&-divider {
border: none;
height: 1px;
background: rgba(0, 0, 0, 0.1);
margin: 6px 1px 5px;
padding: 0;
}
&-list {
list-style: none;
margin: 0;
padding: 0;
width: 185px;
}
&-item {
position: relative;
> a {
white-space: nowrap;
display: block;
padding: 0 20px;
border-top: 1px solid rgba(0, 0, 0, 0);
border-bottom: 1px solid rgba(0, 0, 0, 0);
color: inherit;
}
&:hover > a {
text-decoration: none;
color: #fff;
background: -webkit-linear-gradient(top, #648bf5, #2866f2);
background: linear-gradient(to bottom, #648bf5 0%, #2866f2 100%);
border-top: 1px solid #5a82eb;
border-bottom: 1px solid #1758e7;
}
&.is-disabled > a {
color: #999;
pointer-events: none;
}
&.has-child > a::after {
content: '';
width: 0;
height: 0;
border-width: 4px 7px;
border-style: solid;
border-color: transparent transparent transparent rgba(0, 0, 0, 0.75);
text-shadow: 0px 4px 9px rgba(0, 0, 0, 0.34);
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
}
&.has-child:hover > a::after {
border-color: transparent transparent transparent #fff;
}
}
&-subList {
display: none;
list-style: none;
}
&-item:hover > &-subList {
display: block;
animation-duration: 0.35s;
animation-fill-mode: both;
animation-name: contextMenuIn;
display: block;
position: absolute;
left: 100%;
top: -3px;
margin: 0;
padding: 4px 0 5px;
background: rgba(239, 239, 239, 0.95);
box-shadow: 0px 4px 9px rgba(0, 0, 0, 0.34);
border-radius: 7px;
color: rgba(0, 0, 0, 0.75);
font-family: -apple-system, Lucida Grande;
font-size: 14px;
line-height: 15px;
&::before {
display: block;
position: absolute;
content: '';
top: -1px;
left: -1px;
bottom: -1px;
right: -1px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.125);
z-index: -1;
}
}
&-overlay {
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 1;
bottom: 0;
background: transparent;
}
}

View File

@ -517,6 +517,7 @@ $Card-actions-onChecked-onHover-bg: $white;
@import '../components/dropdown';
@import '../components/collapse';
@import '../components/color';
@import '../components/context-menu';
@import '../components/wizard';
@import '../components/crud';
@import '../components/table';

View File

@ -123,33 +123,33 @@ $Wizard-steps-li-onActive-bg: $Panel-bg;
button[disabled],
html input[disabled] {
cursor: not-allowed;
cursor: not-allowed;
}
input {
color: $text-color;
background: $Form-input-bg;
color: $text-color;
background: $Form-input-bg;
}
pre {
color: $text-color;
background: $background;
color: $text-color;
background: $background;
}
.rdtPicker {
background: $background;
background: $background;
}
.rdtPicker th {
border-bottom: none;
border-bottom: none;
}
.fr-toolbar {
background: $background;
background: $background;
}
.markdown-body {
color: $text-color;
color: $text-color;
}
@import '../functions';
@ -184,7 +184,8 @@ pre {
@import '../components/button-group';
@import '../components/dropdown';
@import '../components/collapse';
@import "../components/color";
@import '../components/color';
@import '../components/context-menu';
@import '../components/wizard';
@import '../components/crud';
@import '../components/table';
@ -216,7 +217,7 @@ pre {
@import '../components/form/date';
@import '../components/form/date-range';
@import '../components/form/image';
@import "../components/form/file";
@import '../components/form/file';
@import '../components/form/editor';
@import '../components/form/rich-text';
@import '../components/form/range';
@ -235,4 +236,4 @@ pre {
@import '../components/form/nested-select';
@import '../components/form/icon-picker';
@import '../utilities';
@import '../utilities';

View File

@ -1,7 +1,7 @@
// 默认主题
// 更新时间: 2019-08-14 14:31:00
$ns: "a-";
$ns: 'a-';
$primary: #7266ba;
$info: #23b7e5;
@ -17,87 +17,88 @@ $link-color: $info;
$Form-input-borderColor: #cfdadd;
@import "../functions";
@import "../variables";
@import "../mixins";
@import "../base/reset";
@import "../base/normalize";
@import "../base/typography";
@import '../functions';
@import '../variables';
@import '../mixins';
@import '../base/reset';
@import '../base/normalize';
@import '../base/typography';
@import "../layout/layout";
@import "../layout/grid";
@import "../layout/aside";
@import "../layout/hbox";
@import "../layout/vbox";
@import "../components/modal";
@import "../components/drawer";
@import "../components/tooltip";
@import "../components/popover";
@import "../components/toast";
@import "../components/alert";
@import "../components/tabs";
@import "../components/nav";
@import "../components/page";
@import "../components/remark";
@import "../components/chart";
@import "../components/video";
@import "../components/audio";
@import "../components/panel";
@import "../components/service";
@import "../components/spinner";
@import "../components/button";
@import "../components/button-group";
@import "../components/dropdown";
@import "../components/collapse";
@import "../components/color";
@import "../components/wizard";
@import "../components/crud";
@import "../components/table";
@import "../components/list";
@import "../components/cards";
@import "../components/card";
@import "../components/quick-edit";
@import "../components/popoverable";
@import "../components/copyable";
@import "../components/divider";
@import "../components/pagination";
@import "../components/wrapper";
@import "../components/status";
@import "../components/carousel";
@import '../layout/layout';
@import '../layout/grid';
@import '../layout/aside';
@import '../layout/hbox';
@import '../layout/vbox';
@import '../components/modal';
@import '../components/drawer';
@import '../components/tooltip';
@import '../components/popover';
@import '../components/toast';
@import '../components/alert';
@import '../components/tabs';
@import '../components/nav';
@import '../components/page';
@import '../components/remark';
@import '../components/chart';
@import '../components/video';
@import '../components/audio';
@import '../components/panel';
@import '../components/service';
@import '../components/spinner';
@import '../components/button';
@import '../components/button-group';
@import '../components/dropdown';
@import '../components/collapse';
@import '../components/color';
@import '../components/context-menu';
@import '../components/wizard';
@import '../components/crud';
@import '../components/table';
@import '../components/list';
@import '../components/cards';
@import '../components/card';
@import '../components/quick-edit';
@import '../components/popoverable';
@import '../components/copyable';
@import '../components/divider';
@import '../components/pagination';
@import '../components/wrapper';
@import '../components/status';
@import '../components/carousel';
@import "../components/form/fieldset";
@import "../components/form/group";
@import "../components/form/input-group";
@import "../components/form/text";
@import "../components/form/textarea";
@import "../components/form/checks";
@import "../components/form/city";
@import "../components/form/switch";
@import "../components/form/number";
@import "../components/form/select";
@import "../components/form/list";
@import "../components/form/matrix";
@import "../components/form/color";
@import "../components/form/date";
@import "../components/form/date-range";
@import "../components/form/image";
@import "../components/form/file";
@import "../components/form/editor";
@import "../components/form/rich-text";
@import "../components/form/range";
@import "../components/form/repeat";
@import "../components/form/tree";
@import "../components/form/tree-select";
@import "../components/form/combo";
@import "../components/form/sub-form";
@import "../components/form/chained-select";
@import "../components/form/picker";
@import "../components/form/qr-code";
@import "../components/form/tag";
@import "../components/form/rating";
@import "../components/form/form";
@import "../components/form/transfer-select";
@import "../components/form/nested-select";
@import "../components/form/icon-picker";
@import '../components/form/fieldset';
@import '../components/form/group';
@import '../components/form/input-group';
@import '../components/form/text';
@import '../components/form/textarea';
@import '../components/form/checks';
@import '../components/form/city';
@import '../components/form/switch';
@import '../components/form/number';
@import '../components/form/select';
@import '../components/form/list';
@import '../components/form/matrix';
@import '../components/form/color';
@import '../components/form/date';
@import '../components/form/date-range';
@import '../components/form/image';
@import '../components/form/file';
@import '../components/form/editor';
@import '../components/form/rich-text';
@import '../components/form/range';
@import '../components/form/repeat';
@import '../components/form/tree';
@import '../components/form/tree-select';
@import '../components/form/combo';
@import '../components/form/sub-form';
@import '../components/form/chained-select';
@import '../components/form/picker';
@import '../components/form/qr-code';
@import '../components/form/tag';
@import '../components/form/rating';
@import '../components/form/form';
@import '../components/form/transfer-select';
@import '../components/form/nested-select';
@import '../components/form/icon-picker';
@import "../utilities";
@import '../utilities';

View File

@ -0,0 +1,199 @@
import {ClassNamesFn, themeable} from '../theme';
import React from 'react';
import {render} from 'react-dom';
import {autobind} from '../utils/helper';
import Transition, {
ENTERED,
ENTERING,
EXITING
} from 'react-transition-group/Transition';
import {Portal} from 'react-overlays';
const fadeStyles: {
[propName: string]: string;
} = {
[ENTERING]: 'in',
[ENTERED]: 'in',
[EXITING]: 'out'
};
interface ContextMenuProps {
className?: string;
classPrefix: string;
classnames: ClassNamesFn;
container?: HTMLElement | null | (() => HTMLElement);
}
export type MenuItem = {
label: string;
icon?: string;
disabled?: boolean;
children?: Array<MenuItem | MenuDivider>;
data?: any;
onSelect?: (data: any) => void;
};
export type MenuDivider = '|';
interface ContextMenuState {
isOpened: boolean;
menus: Array<MenuItem | MenuDivider>;
x: number;
y: number;
}
export class ContextMenu extends React.Component<
ContextMenuProps,
ContextMenuState
> {
static instance: any = null;
static getInstance() {
if (!ContextMenu.instance) {
const container = document.body;
const div = document.createElement('div');
container.appendChild(div);
render(<ThemedContextMenu />, div);
}
return ContextMenu.instance;
}
state = {
isOpened: false,
menus: [],
x: -99999,
y: -99999
};
menuRef: React.RefObject<HTMLDivElement> = React.createRef();
componentWillMount() {
ContextMenu.instance = this;
}
componentDidMount() {
document.body.addEventListener('click', this.handleOutClick, true);
}
componentWillUnmount() {
ContextMenu.instance = null;
document.body.removeEventListener('click', this.handleOutClick, true);
}
@autobind
openContextMenus(info: {x: number; y: number}, menus: Array<MenuItem>) {
this.setState({
isOpened: true,
x: info.x,
y: info.y,
menus: menus
});
}
@autobind
close() {
this.setState({
isOpened: false,
x: -99999,
y: -99999,
menus: []
});
}
@autobind
handleOutClick(e: Event) {
if (
!e.target ||
!this.menuRef.current ||
this.menuRef.current.contains(e.target as HTMLElement)
) {
return;
}
this.close();
}
handleClick(item: MenuItem) {
item.disabled ||
(Array.isArray(item.children) && item.children.length) ||
this.setState(
{
isOpened: false,
x: -99999,
y: -99999,
menus: []
},
() => (item.onSelect ? item.onSelect(item.data) : null)
);
}
renderMenus(menus: Array<MenuItem | MenuDivider>) {
const {classnames: cx} = this.props;
return menus.map((item, index) => {
if (item === '|') {
return <li key={index} className={cx('ContextMenu-divider')} />;
}
const hasChildren = Array.isArray(item.children) && item.children.length;
return (
<li
key={item.label}
className={cx('ContextMenu-item', {
'has-child': hasChildren,
'is-disabled': item.disabled
})}
>
<a onClick={this.handleClick.bind(this, item)}>
{item.icon ? <span className={item.icon} /> : null}
{item.label}
</a>
{hasChildren ? (
<ul className={cx('ContextMenu-subList')}>
{this.renderMenus(item.children!)}
</ul>
) : null}
</li>
);
});
}
render() {
const {className, container, classnames: cx} = this.props;
return (
<Portal container={container}>
<Transition
mountOnEnter
unmountOnExit
in={this.state.isOpened}
timeout={500}
>
{(status: string) => (
<div
ref={this.menuRef}
role="contextmenu"
className={cx('ContextMenu', className)}
>
<div
style={{left: `${this.state.x}px`, top: `${this.state.y}px`}}
className={cx(`ContextMenu-menu`, fadeStyles[status])}
>
<ul className={cx('ContextMenu-list')}>
{this.renderMenus(this.state.menus)}
</ul>
</div>
</div>
)}
</Transition>
</Portal>
);
}
}
export const ThemedContextMenu = themeable(ContextMenu);
export default ThemedContextMenu;
export function openContextMenus(
info: Event | {x: number; y: number},
menus: Array<MenuItem | MenuDivider>
) {
return ContextMenu.getInstance().openContextMenus(info, menus);
}

View File

@ -6,6 +6,7 @@
import NotFound from './404';
import {default as Alert, alert, confirm} from './Alert';
import {default as ContextMenu, openContextMenus} from './ContextMenu';
import AsideNav from './AsideNav';
import Button from './Button';
import Checkbox from './Checkbox';
@ -43,6 +44,8 @@ export {
NotFound,
Alert as AlertComponent,
alert,
ContextMenu,
openContextMenus,
Alert2,
confirm,
AsideNav,

View File

@ -25,6 +25,8 @@ import {
NotFound,
AlertComponent,
alert,
ContextMenu,
openContextMenus,
Alert2,
confirm,
AsideNav,
@ -197,6 +199,8 @@ export {
NotFound,
AlertComponent,
alert,
ContextMenu,
openContextMenus,
Alert2,
confirm,
AsideNav,

View File

@ -19,16 +19,10 @@ export default class ListControl extends React.Component<ListProps, any> {
};
handleDBClick(option: Option, e: React.MouseEvent<HTMLElement>) {
this.props.onToggle(option);
// 差不多有 250ms 的防抖。
setTimeout(
() =>
this.props.onAction(null, {
type: 'submit'
}),
250
);
this.props.onToggle(option, false, true);
this.props.onAction(null, {
type: 'submit'
});
}
handleClick(option: Option, e: React.MouseEvent<HTMLElement>) {

View File

@ -43,7 +43,11 @@ export interface OptionsConfig extends OptionsBasicConfig {
export interface OptionsControlProps extends FormControlProps, OptionProps {
source?: Api;
name?: string;
onToggle: (option: Option, submitOnChange?: boolean) => void;
onToggle: (
option: Option,
submitOnChange?: boolean,
changeImmediately?: boolean
) => void;
onToggleAll: () => void;
selectedOptions: Array<Option>;
setOptions: (value: Array<any>) => void;
@ -308,7 +312,11 @@ export function registerOptionsControl(config: OptionsConfig) {
}
@autobind
handleToggle(option: Option, submitOnChange?: boolean) {
handleToggle(
option: Option,
submitOnChange?: boolean,
changeImmediately?: boolean
) {
const {
onChange,
joinValues,
@ -362,7 +370,7 @@ export function registerOptionsControl(config: OptionsConfig) {
}
}
onChange && onChange(newValue, submitOnChange);
onChange && onChange(newValue, submitOnChange, changeImmediately);
}
@autobind