Merge pull request #211 from ONLYOFFICE/feature/contextmenu-refactoring

Feature/contextmenu refactoring
This commit is contained in:
Alexey Safronov 2021-04-02 19:45:50 +03:00 committed by GitHub
commit ae6fd12605
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1050 additions and 241 deletions

View File

@ -30,8 +30,6 @@ class ContextMenuButton extends React.Component {
isOpen: props.opened,
data: props.data,
displayType,
offsetX: props.manualX,
offsetY: props.manualY,
};
this.throttledResize = throttle(this.resize, 300);
}
@ -73,10 +71,6 @@ class ContextMenuButton extends React.Component {
this.toggle(this.props.opened);
}
if (this.props.manualX !== prevProps.manualX) {
this.onContextClick();
}
if (this.props.opened && this.state.displayType === "aside") {
window.addEventListener("popstate", this.popstate, false);
}
@ -86,20 +80,6 @@ class ContextMenuButton extends React.Component {
}
}
onContextClick = () => {
if (this.props.isDisabled) {
this.stopAction;
return;
}
this.setState({
data: this.props.getData(),
isOpen: !this.state.isOpen,
offsetX: this.props.manualX,
offsetY: this.props.manualY,
});
};
onIconButtonClick = () => {
if (this.props.isDisabled) {
this.stopAction;
@ -110,8 +90,6 @@ class ContextMenuButton extends React.Component {
{
data: this.props.getData(),
isOpen: !this.state.isOpen,
offsetX: "0px",
offsetY: "100%",
},
() =>
!this.props.isDisabled &&
@ -194,15 +172,12 @@ class ContextMenuButton extends React.Component {
/>
{displayType === "dropdown" ? (
<DropDown
id="contextMenu"
manualX={offsetX}
manualY={offsetY}
directionX={directionX}
directionY={directionY}
open={isOpen}
clickOutsideAction={this.clickOutsideAction}
columnCount={columnCount}
withBackdrop={isMobile}
withBackdrop={!!isMobile}
>
{this.state.data.map(
(item, index) =>
@ -299,10 +274,6 @@ ContextMenuButton.propTypes = {
directionX: PropTypes.string,
/** Direction Y */
directionY: PropTypes.string,
/** Manual X padding */
manualX: PropTypes.string,
/** Manual Y padding */
manualY: PropTypes.string,
/** Accepts class */
className: PropTypes.string,
/** Accepts id */

View File

@ -1,14 +1,10 @@
import React from "react";
import React, { useRef } from "react";
import RowContainer from "../row-container";
import RowContent from "../row-content";
import Row from "../row";
import ContextMenu from "./index";
export default {
title: "Components/ContextMenu",
component: ContextMenu,
subcomponents: { RowContainer, Row, RowContent },
parameters: {
docs: {
description: {
@ -24,37 +20,112 @@ In particular case, state is created containing options for particular Row eleme
},
};
const getRndString = (n) =>
Math.random()
.toString(36)
.substring(2, n + 2);
const Template = (args) => {
const cm = useRef(null);
const items = [
{
label: "Edit",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Preview",
icon: "/static/images/catalog.folder.react.svg",
},
{
separator: true,
},
{
label: "Sharing settings",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Link for portal users",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Copy external link",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Send by e-mail",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Version history",
icon: "/static/images/catalog.folder.react.svg",
items: [
{
label: "Show version history",
},
{
label: "Finalize version",
},
{
label: "Unblock / Check-in",
},
],
},
{
separator: true,
},
{
label: "Make as favorite",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Download",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Download as",
icon: "/static/images/catalog.folder.react.svg",
},
{
label: "Move or copy",
icon: "/static/images/catalog.folder.react.svg",
items: [
{
label: "Move to",
},
{
label: "Copy",
},
{
label: "Duplicate",
},
],
},
{
label: "Rename",
icon: "/static/images/catalog.folder.react.svg",
disabled: true,
},
{
separator: true,
},
{
label: "Quit",
icon: "/static/images/catalog.folder.react.svg",
},
];
const array = Array.from(Array(10).keys());
const Template = (args) => (
<RowContainer {...args} manualHeight="300px">
{array.map((item, index) => {
return (
<Row
key={`${item + 1}`}
contextOptions={
index !== 3
? [
{ key: 1, label: getRndString(5) },
{ key: 2, label: getRndString(5) },
{ key: 3, label: getRndString(5) },
{ key: 4, label: getRndString(5) },
]
: []
}
>
<RowContent>
<span>{getRndString(5)}</span>
<></>
</RowContent>
</Row>
);
})}
</RowContainer>
);
return (
<div>
<ContextMenu model={items} ref={cm}></ContextMenu>
<div
style={{
width: "200px",
height: "200px",
backgroundColor: "red",
display: "inline-block",
}}
onContextMenu={(e) => cm.current.show(e)}
>
{""}
</div>
</div>
);
};
export const Default = Template.bind({});

View File

@ -13,7 +13,7 @@ describe("<ContextMenu />", () => {
expect(wrapper).toExist();
});
it("componentWillUnmount() test unmount", () => {
/* it("componentWillUnmount() test unmount", () => {
const wrapper = mount(<ContextMenu {...baseProps} />);
wrapper.unmount();
@ -82,5 +82,5 @@ describe("<ContextMenu />", () => {
wrapper.setState({ visible: true });
expect(wrapper.getDOMNode().style).toHaveProperty("color", "red");
});
}); */
});

View File

@ -1,134 +1,589 @@
import React from "react";
import React, { Component } from "react";
import PropTypes from "prop-types";
import DropDownItem from "../drop-down-item";
import DropDown from "../drop-down";
import DomHelpers from "../utils/domHelpers";
import ObjectUtils from "../utils/objectUtils";
import { classNames } from "../utils/classNames";
import { CSSTransition } from "react-transition-group";
import { ReactSVG } from "react-svg";
import Portal from "../portal";
import StyledContextMenu from "./styled-context-menu";
import ArrowIcon from "./svg/arrow.right.react.svg";
class ContextMenu extends React.PureComponent {
class ContextMenuSub extends Component {
constructor(props) {
super(props);
this.state = {
activeItem: null,
};
this.onEnter = this.onEnter.bind(this);
this.submenuRef = React.createRef();
}
onItemMouseEnter(e, item) {
if (item.disabled) {
e.preventDefault();
return;
}
this.setState({
activeItem: item,
});
}
onItemClick(e, item) {
if (item.disabled) {
e.preventDefault();
return;
}
if (!item.url) {
e.preventDefault();
}
if (item.onClick) {
item.onClick({
originalEvent: e,
});
}
if (!item.items) {
this.props.onLeafClick(e);
}
}
position() {
const parentItem = this.submenuRef.current.parentElement;
const containerOffset = DomHelpers.getOffset(
this.submenuRef.current.parentElement
);
const viewport = DomHelpers.getViewport();
const sublistWidth = this.submenuRef.current.offsetParent
? this.submenuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(this.submenuRef.current);
const itemOuterWidth = DomHelpers.getOuterWidth(parentItem.children[0]);
this.submenuRef.current.style.top = "0px";
if (
parseInt(containerOffset.left, 10) + itemOuterWidth + sublistWidth >
viewport.width - DomHelpers.calculateScrollbarWidth()
) {
this.submenuRef.current.style.left = -1 * sublistWidth + "px";
} else {
this.submenuRef.current.style.left = itemOuterWidth + "px";
}
}
onEnter() {
this.position();
}
isActive() {
return this.props.root || !this.props.resetMenu;
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.resetMenu === true) {
return {
activeItem: null,
};
}
return null;
}
componentDidUpdate() {
if (this.isActive()) {
this.position();
}
}
renderSeparator(index) {
return (
<li
key={"separator_" + index}
className="p-menu-separator"
role="separator"
></li>
);
}
renderSubmenu(item) {
if (item.items) {
return (
<ContextMenuSub
model={item.items}
resetMenu={item !== this.state.activeItem}
onLeafClick={this.props.onLeafClick}
/>
);
}
return null;
}
renderMenuitem(item, index) {
if (item.disabled) return; //TODO: Not render disabled items
const active = this.state.activeItem === item;
const className = classNames(
"p-menuitem",
{ "p-menuitem-active": active },
item.className
);
const linkClassName = classNames("p-menuitem-link", {
"p-disabled": item.disabled,
});
const iconClassName = classNames("p-menuitem-icon", {
"p-disabled": item.disabled,
});
const submenuIconClassName = "p-submenu-icon";
const icon = item.icon && (
<ReactSVG
wrapper="span"
className={iconClassName}
src={item.icon}
></ReactSVG>
);
const label = item.label && (
<span className="p-menuitem-text">{item.label}</span>
);
const submenuIcon = item.items && (
<ArrowIcon className={submenuIconClassName} />
);
const submenu = this.renderSubmenu(item);
let content = (
<a
href={item.url || "#"}
className={linkClassName}
target={item.target}
onClick={(event) => this.onItemClick(event, item, index)}
role="menuitem"
>
{icon}
{label}
{submenuIcon}
</a>
);
if (item.template) {
const defaultContentOptions = {
onClick: (event) => this.onItemClick(event, item, index),
className: linkClassName,
labelClassName: "p-menuitem-text",
iconClassName,
submenuIconClassName,
element: content,
props: this.props,
active,
};
content = ObjectUtils.getJSXElement(
item.template,
item,
defaultContentOptions
);
}
return (
<li
key={item.label + "_" + index}
role="none"
className={className}
style={item.style}
onMouseEnter={(event) => this.onItemMouseEnter(event, item)}
>
{content}
{submenu}
</li>
);
}
renderItem(item, index) {
if (item.isSeparator) return this.renderSeparator(index);
else return this.renderMenuitem(item, index);
}
renderMenu() {
if (this.props.model) {
return this.props.model.map((item, index) => {
return this.renderItem(item, index);
});
}
return null;
}
render() {
const className = classNames({ "p-submenu-list": !this.props.root });
const submenu = this.renderMenu();
const isActive = this.isActive();
return (
<CSSTransition
nodeRef={this.submenuRef}
classNames="p-contextmenusub"
in={isActive}
timeout={{ enter: 0, exit: 0 }}
unmountOnExit={true}
onEnter={this.onEnter}
>
<ul ref={this.submenuRef} className={className}>
{submenu}
</ul>
</CSSTransition>
);
}
}
ContextMenuSub.propTypes = {
model: PropTypes.any,
root: PropTypes.bool,
className: PropTypes.string,
resetMenu: PropTypes.bool,
onLeafClick: PropTypes.func,
};
ContextMenuSub.defaultProps = {
model: null,
root: false,
className: null,
resetMenu: false,
onLeafClick: null,
};
class ContextMenu extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
reshow: false,
resetMenu: false,
};
this.onMenuClick = this.onMenuClick.bind(this);
this.onLeafClick = this.onLeafClick.bind(this);
this.onMenuMouseEnter = this.onMenuMouseEnter.bind(this);
this.onEnter = this.onEnter.bind(this);
this.onEntered = this.onEntered.bind(this);
this.onExit = this.onExit.bind(this);
this.onExited = this.onExited.bind(this);
this.menuRef = React.createRef();
}
onMenuClick() {
this.setState({
resetMenu: false,
});
}
onMenuMouseEnter() {
this.setState({
resetMenu: false,
});
}
show(e) {
if (!(e instanceof Event)) {
e.persist();
}
e.stopPropagation();
e.preventDefault();
this.currentEvent = e;
if (this.state.visible) {
this.setState({ reshow: true });
} else {
this.setState({ visible: true }, () => {
if (this.props.onShow) {
this.props.onShow(this.currentEvent);
}
});
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.visible && prevState.reshow !== this.state.reshow) {
let event = this.currentEvent;
this.setState(
{
visible: false,
reshow: false,
rePosition: false,
resetMenu: true,
},
() => this.show(event)
);
}
}
hide(e) {
if (!(e instanceof Event)) {
e.persist();
}
this.currentEvent = e;
this.setState({ visible: false, reshow: false }, () => {
if (this.props.onHide) {
this.props.onHide(this.currentEvent);
}
});
}
onEnter() {
if (this.props.autoZIndex) {
this.menuRef.current.style.zIndex = String(
this.props.baseZIndex + DomHelpers.generateZIndex()
);
}
this.position(this.currentEvent);
}
onEntered() {
this.bindDocumentListeners();
}
onExit() {
this.currentEvent = null;
this.unbindDocumentListeners();
}
onExited() {
DomHelpers.revertZIndex();
}
position(event) {
if (event) {
let left = event.pageX + 1;
let top = event.pageY + 1;
let width = this.menuRef.current.offsetParent
? this.menuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(this.menuRef.current);
let height = this.menuRef.current.offsetParent
? this.menuRef.current.offsetHeight
: DomHelpers.getHiddenElementOuterHeight(this.menuRef.current);
let viewport = DomHelpers.getViewport();
//flip
if (left + width - document.body.scrollLeft > viewport.width) {
left -= width;
}
//flip
if (top + height - document.body.scrollTop > viewport.height) {
top -= height;
}
//fit
if (left < document.body.scrollLeft) {
left = document.body.scrollLeft;
}
//fit
if (top < document.body.scrollTop) {
top = document.body.scrollTop;
}
this.menuRef.current.style.left = left + "px";
this.menuRef.current.style.top = top + "px";
}
}
onLeafClick(e) {
this.setState({
resetMenu: true,
});
this.hide(e);
e.stopPropagation();
}
isOutsideClicked(e) {
return (
this.menuRef &&
this.menuRef.current &&
!(
this.menuRef.current.isSameNode(e.target) ||
this.menuRef.current.contains(e.target)
)
);
}
bindDocumentListeners() {
this.bindDocumentResizeListener();
this.bindDocumentClickListener();
}
unbindDocumentListeners() {
this.unbindDocumentResizeListener();
this.unbindDocumentClickListener();
}
bindDocumentClickListener() {
if (!this.documentClickListener) {
this.documentClickListener = (e) => {
if (this.isOutsideClicked(e) && e.button !== 2) {
this.hide(e);
this.setState({
resetMenu: true,
});
}
};
document.addEventListener("click", this.documentClickListener);
}
}
bindDocumentContextMenuListener() {
if (!this.documentContextMenuListener) {
this.documentContextMenuListener = (e) => {
this.show(e);
};
document.addEventListener(
"contextmenu",
this.documentContextMenuListener
);
}
}
bindDocumentResizeListener() {
if (!this.documentResizeListener) {
this.documentResizeListener = (e) => {
if (this.state.visible) {
this.hide(e);
}
};
window.addEventListener("resize", this.documentResizeListener);
}
}
unbindDocumentClickListener() {
if (this.documentClickListener) {
document.removeEventListener("click", this.documentClickListener);
this.documentClickListener = null;
}
}
unbindDocumentContextMenuListener() {
if (this.documentContextMenuListener) {
document.removeEventListener(
"contextmenu",
this.documentContextMenuListener
);
this.documentContextMenuListener = null;
}
}
unbindDocumentResizeListener() {
if (this.documentResizeListener) {
window.removeEventListener("resize", this.documentResizeListener);
this.documentResizeListener = null;
}
}
componentDidMount() {
this.container =
document.getElementById(this.props.targetAreaId) || document;
this.container.addEventListener("contextmenu", this.handleContextMenu);
if (this.props.global) {
this.bindDocumentContextMenuListener();
}
}
componentWillUnmount() {
this.container.removeEventListener("contextmenu", this.handleContextMenu);
this.unbindDocumentListeners();
this.unbindDocumentContextMenuListener();
DomHelpers.revertZIndex();
}
moveMenu = (e) => {
const menu = document.getElementById(this.props.id);
const bounds =
this.container !== document && this.container.getBoundingClientRect();
const clickX = e.clientX - bounds.left;
const clickY = e.clientY - bounds.top;
const containerWidth = this.container.offsetWidth;
const containerHeight = this.container.offsetHeight;
const menuWidth = (menu && menu.offsetWidth) || 180;
const menuHeight = menu && menu.offsetHeight;
const right = containerWidth - clickX < menuWidth && clickX > menuWidth;
const bottom = containerHeight - clickY < menuHeight && clickY > menuHeight;
let newTop = `0px`;
let newLeft = `0px`;
newLeft = right ? `${clickX - menuWidth - 8}px` : `${clickX + 8}px`;
newTop = bottom ? `${clickY - menuHeight}px` : `${clickY}px`;
if (menu) {
menu.style.top = newTop;
menu.style.left = newLeft;
}
};
handleContextMenu = (e) => {
if (e) {
e.preventDefault();
this.handleClick(e);
}
this.setState(
{
visible: true,
},
() => this.moveMenu(e)
renderContextMenu() {
const className = classNames(
"p-contextmenu p-component",
this.props.className
);
};
handleClick = (e) => {
const { visible } = this.state;
const menu = document.getElementById(this.props.id);
const wasOutside = e.target ? !(e.target.contains === menu) : true;
if (wasOutside && visible) this.setState({ visible: false });
};
itemClick = (action, e) => {
action && action(e);
this.setState({ visible: false });
};
render() {
//console.log('ContextMenu render', this.props);
const { visible } = this.state;
const { options, id, className, style, withBackdrop } = this.props;
return (
((visible && options) || null) && (
<DropDown
id={id}
className={className}
style={style}
open={visible}
clickOutsideAction={this.handleClick}
withBackdrop={withBackdrop}
<StyledContextMenu>
<CSSTransition
nodeRef={this.menuRef}
classNames="p-contextmenu"
in={this.state.visible}
timeout={{ enter: 250, exit: 0 }}
unmountOnExit
onEnter={this.onEnter}
onEntered={this.onEntered}
onExit={this.onExit}
onExited={this.onExited}
>
{options.map((item) => {
if (item && item.key !== undefined) {
return (
<DropDownItem
key={item.key}
{...item}
onClick={this.itemClick.bind(this, item.onClick)}
/>
);
}
})}
</DropDown>
)
<div
ref={this.menuRef}
id={this.props.id}
className={className}
style={this.props.style}
onClick={this.onMenuClick}
onMouseEnter={this.onMenuMouseEnter}
>
<ContextMenuSub
model={this.props.model}
root
resetMenu={this.state.resetMenu}
onLeafClick={this.onLeafClick}
/>
</div>
</CSSTransition>
</StyledContextMenu>
);
}
render() {
const element = this.renderContextMenu();
return <Portal element={element} appendTo={this.props.appendTo} />;
}
}
ContextMenu.propTypes = {
/** DropDownItems collection */
options: PropTypes.array,
/** Id of container apply to */
targetAreaId: PropTypes.string,
/** Accepts class */
className: PropTypes.string,
/** Accepts id */
/** Unique identifier of the element */
id: PropTypes.string,
/** Accepts css style */
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
/** Used to display backdrop */
withBackdrop: PropTypes.bool,
/** An array of menuitems */
model: PropTypes.array,
/** Inline style of the component */
style: PropTypes.object,
/** Style class of the component */
className: PropTypes.string,
/** Attaches the menu to document instead of a particular item */
global: PropTypes.bool,
/** Base zIndex value to use in layering */
autoZIndex: PropTypes.bool,
/** Whether to automatically manage layering */
baseZIndex: PropTypes.number,
/** DOM element instance where the menu should be mounted */
appendTo: PropTypes.any,
/** Callback to invoke when a popup menu is shown */
onShow: PropTypes.func,
/** Callback to invoke when a popup menu is hidden */
onHide: PropTypes.func,
};
ContextMenu.defaultProps = {
options: [],
id: "contextMenu",
withBackdrop: true,
id: null,
model: null,
style: null,
className: null,
global: false,
autoZIndex: true,
baseZIndex: 0,
appendTo: null,
onShow: null,
onHide: null,
};
export default ContextMenu;

View File

@ -0,0 +1,136 @@
import styled from "styled-components";
import Base from "../themes/base";
const StyledContextMenu = styled.div`
.p-contextmenu {
position: absolute;
background: ${(props) => props.theme.dropDown.background};
border-radius: ${(props) => props.theme.dropDown.borderRadius};
-moz-border-radius: ${(props) => props.theme.dropDown.borderRadius};
-webkit-border-radius: ${(props) => props.theme.dropDown.borderRadius};
box-shadow: ${(props) => props.theme.dropDown.boxShadow};
-moz-box-shadow: ${(props) => props.theme.dropDown.boxShadow};
-webkit-box-shadow: ${(props) => props.theme.dropDown.boxShadow};
padding: 4px 0px;
}
.p-contextmenu ul {
margin: 0;
padding: 0;
list-style: none;
}
.p-contextmenu .p-submenu-list {
position: absolute;
background: ${(props) => props.theme.dropDown.background};
border-radius: ${(props) => props.theme.dropDown.borderRadius};
-moz-border-radius: ${(props) => props.theme.dropDown.borderRadius};
-webkit-border-radius: ${(props) => props.theme.dropDown.borderRadius};
box-shadow: ${(props) => props.theme.dropDown.boxShadow};
-moz-box-shadow: ${(props) => props.theme.dropDown.boxShadow};
-webkit-box-shadow: ${(props) => props.theme.dropDown.boxShadow};
padding: 4px 0px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 8px;
margin-top: -4px;
}
.p-contextmenu .p-menuitem-link {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
overflow: hidden;
position: relative;
border: ${(props) => props.theme.dropDownItem.border};
margin: ${(props) => props.theme.dropDownItem.margin};
padding: ${(props) => props.theme.dropDownItem.padding};
font-family: ${(props) => props.theme.fontFamily};
font-style: normal;
background: none;
user-select: none;
outline: 0 !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-weight: ${(props) => props.theme.dropDownItem.fontWeight};
font-size: ${(props) => props.theme.dropDownItem.fontSize};
color: ${(props) => props.theme.dropDownItem.color};
text-transform: none;
&:hover {
background-color: ${(props) =>
props.noHover
? props.theme.dropDownItem.backgroundColor
: props.theme.dropDownItem.hoverBackgroundColor};
}
&.p-disabled {
color: ${(props) => props.theme.dropDownItem.disableColor};
&:hover {
cursor: default;
background-color: ${(props) =>
props.theme.dropDownItem.hoverDisabledBackgroundColor};
}
}
}
.p-contextmenu .p-menuitem-text {
line-height: ${(props) => props.theme.dropDownItem.lineHeight};
}
.p-contextmenu .p-menu-separator {
cursor: default;
padding: 0px 16px;
margin: 4px 16px 4px;
border-bottom: 1px solid #eceef1;
width: calc(90%-32px);
&:hover {
cursor: default;
}
}
.p-contextmenu .p-menuitem {
position: relative;
margin: ${(props) => props.theme.dropDownItem.margin};
}
.p-menuitem-icon {
max-height: ${(props) => props.theme.dropDownItem.lineHeight};
path {
fill: ${(props) => props.theme.dropDownItem.icon.color};
}
&.p-disabled {
path {
fill: ${(props) => props.theme.dropDownItem.icon.disableColor};
}
}
margin-right: 8px;
}
.p-submenu-icon {
margin-left: auto;
}
.p-contextmenu-enter {
opacity: 0;
}
.p-contextmenu-enter-active {
opacity: 1;
transition: opacity 250ms;
}
`;
StyledContextMenu.defaultProps = {
theme: Base,
};
export default StyledContextMenu;

View File

@ -0,0 +1,3 @@
<svg width="5" height="9" viewBox="0 0 5 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.07786 4.50079L0.468338 1.45635C0.216742 1.16282 0.250735 0.72091 0.544263 0.469314C0.837792 0.217719 1.2797 0.251712 1.5313 0.54524L4.5313 4.04524C4.75599 4.30738 4.75599 4.69421 4.5313 4.95635L1.5313 8.45635C1.2797 8.74988 0.837792 8.78387 0.544263 8.53227C0.250735 8.28068 0.216742 7.83877 0.468338 7.54524L3.07786 4.50079Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@ -60,3 +60,4 @@ export { Icons } from "./icons";
export { default as SaveCancelButtons } from "./save-cancel-buttons";
export { default as DragAndDrop } from "./drag-and-drop";
export * as Themes from "./themes";
export { default as Portal } from "./portal";

View File

@ -84,6 +84,7 @@
"react-text-mask": "^5.4.3",
"react-toastify": "^5.5.0",
"react-tooltip": "^3.11.6",
"react-transition-group": "^4.4.1",
"react-virtualized-auto-sizer": "^1.0.3",
"react-window": "^1.8.6",
"react-window-infinite-loader": "^1.0.5",

View File

@ -0,0 +1,50 @@
import { Component } from "react";
import PropTypes from "prop-types";
import ReactDOM from "react-dom";
class Portal extends Component {
constructor(props) {
super(props);
this.state = {
mounted: props.visible,
};
}
domExist() {
return !!(
typeof window !== undefined &&
window.document &&
window.document.createElement
);
}
componentDidMount() {
if (this.domExist() && !this.state.mounted) {
this.setState({ mounted: true });
}
}
render() {
return this.props.element && this.state.mounted
? ReactDOM.createPortal(
this.props.element,
this.props.appendTo || document.body
)
: null;
}
}
Portal.propTypes = {
element: PropTypes.any,
appendTo: PropTypes.any,
visible: PropTypes.bool,
};
Portal.defaultProps = {
element: null,
appendTo: null,
visible: false,
};
export default Portal;

View File

@ -3,6 +3,7 @@ import React from "react";
import Checkbox from "../checkbox";
import ContextMenuButton from "../context-menu-button";
import ContextMenu from "../context-menu";
import {
StyledOptionButton,
StyledContentElement,
@ -16,58 +17,11 @@ class Row extends React.Component {
constructor(props) {
super(props);
this.state = {
contextX: "0px",
contextY: "100%",
contextOpened: false,
};
this.rowRef = React.createRef();
this.cm = React.createRef();
this.row = React.createRef();
}
componentDidMount() {
this.container = this.rowRef.current;
this.container.addEventListener("contextmenu", this.onContextMenu);
}
componentWillUnmount() {
this.container &&
this.container.removeEventListener("contextmenu", this.onContextMenu);
}
onContextMenu = (e) => {
e.preventDefault();
const menu = document.getElementById("contextMenu");
const containerBounds =
this.container !== document && this.container.getBoundingClientRect();
const clickX = containerBounds.right - e.clientX;
const clickY = e.clientY - containerBounds.top;
const containerWidth = this.container.offsetWidth;
const containerHeight = this.container.offsetHeight;
const menuWidth = (menu && menu.offsetWidth) || 180;
const menuHeight = menu && menu.offsetHeight;
const left = containerWidth - clickX > menuWidth && clickX < menuWidth;
const bottom = containerHeight - clickY < menuHeight && clickY > menuHeight;
let newTop = `0px`;
let newRight = `0px`;
newRight = !left ? `${clickX - menuWidth - 8}px` : `${clickX + 8}px`;
newTop = bottom ? `${clickY - menuHeight}px` : `${clickY}px`;
this.setState({
contextOpened: !this.state.contextOpened,
contextX: newRight,
contextY: newTop,
});
};
render() {
//console.log("Row render");
const {
checked,
children,
@ -82,8 +36,6 @@ class Row extends React.Component {
sectionWidth,
} = this.props;
const { contextOpened, contextX, contextY } = this.state;
const renderCheckbox = Object.prototype.hasOwnProperty.call(
this.props,
"checked"
@ -111,8 +63,16 @@ class Row extends React.Component {
return contextOptions;
};
const onContextMenu = (e) => {
rowContextClick && rowContextClick();
if (!this.cm.current.menuRef.current) {
this.row.current.click(e); //TODO: need fix context menu to global
}
this.cm.current.show(e);
};
return (
<StyledRow ref={this.rowRef} {...this.props}>
<StyledRow ref={this.row} {...this.props} onContextMenu={onContextMenu}>
{renderCheckbox && (
<StyledCheckbox>
<Checkbox
@ -135,9 +95,6 @@ class Row extends React.Component {
)}
{renderContext ? (
<ContextMenuButton
manualX={contextX}
manualY={contextY}
opened={contextOpened}
color="#A3A9AE"
hoverColor="#657077"
className="expandButton"
@ -147,6 +104,7 @@ class Row extends React.Component {
) : (
<div className="expandButton"> </div>
)}
<ContextMenu model={contextOptions} ref={this.cm}></ContextMenu>
</StyledOptionButton>
</StyledRow>
);

View File

@ -87,13 +87,4 @@ describe("<Row />", () => {
expect(wrapper.getDOMNode().style).toHaveProperty("color", "red");
});
it("componentWillUnmount() props lifecycle test", () => {
const wrapper = shallow(<Row {...baseProps} />);
const instance = wrapper.instance();
instance.componentWillUnmount();
expect(wrapper).toExist(false);
});
});

View File

@ -0,0 +1,31 @@
export function classNames(...args) {
if (args) {
let classes = [];
for (let i = 0; i < args.length; i++) {
let className = args[i];
if (!className) continue;
const type = typeof className;
if (type === "string" || type === "number") {
classes.push(className);
} else if (type === "object") {
const _classes = Array.isArray(className)
? className
: Object.entries(className).map(([key, value]) =>
!!value ? key : null
);
classes = _classes.length
? classes.concat(_classes.filter((c) => !!c))
: classes;
}
}
return classes.join(" ");
}
return null;
}

View File

@ -0,0 +1,117 @@
export default class DomHelpers {
static getViewport() {
let win = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName("body")[0],
w = win.innerWidth || e.clientWidth || g.clientWidth,
h = win.innerHeight || e.clientHeight || g.clientHeight;
return { width: w, height: h };
}
static getOffset(el) {
if (el) {
let rect = el.getBoundingClientRect();
return {
top:
rect.top +
(window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0),
left:
rect.left +
(window.pageXOffset ||
document.documentElement.scrollLeft ||
document.body.scrollLeft ||
0),
};
}
return {
top: "auto",
left: "auto",
};
}
static getOuterWidth(el, margin) {
if (el) {
let width = el.offsetWidth;
if (margin) {
let style = getComputedStyle(el);
width += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
}
return width;
}
return 0;
}
static getHiddenElementOuterWidth(element) {
if (element) {
element.style.visibility = "hidden";
element.style.display = "block";
let elementWidth = element.offsetWidth;
element.style.display = "none";
element.style.visibility = "visible";
return elementWidth;
}
return 0;
}
static getHiddenElementOuterHeight(element) {
if (element) {
element.style.visibility = "hidden";
element.style.display = "block";
let elementHeight = element.offsetHeight;
element.style.display = "none";
element.style.visibility = "visible";
return elementHeight;
}
return 0;
}
static calculateScrollbarWidth(el) {
if (el) {
let style = getComputedStyle(el);
return (
el.offsetWidth -
el.clientWidth -
parseFloat(style.borderLeftWidth) -
parseFloat(style.borderRightWidth)
);
} else {
if (this.calculatedScrollbarWidth != null)
return this.calculatedScrollbarWidth;
let scrollDiv = document.createElement("div");
scrollDiv.className = "p-scrollbar-measure";
document.body.appendChild(scrollDiv);
let scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
this.calculatedScrollbarWidth = scrollbarWidth;
return scrollbarWidth;
}
}
static generateZIndex() {
this.zIndex = this.zIndex || 1000;
return ++this.zIndex;
}
static revertZIndex() {
this.zIndex = 1000 < this.zIndex ? --this.zIndex : 1000;
}
static getCurrentZIndex() {
return this.zIndex;
}
}

View File

@ -0,0 +1,5 @@
export default class ObjectUtils {
static getJSXElement(obj, ...params) {
return this.isFunction(obj) ? obj(...params) : obj;
}
}

View File

@ -103,6 +103,7 @@ const SimpleFilesRow = (props) => {
setThirdpartyInfo,
setMediaViewerData,
setDragging,
setStartDrag,
startUpload,
onSelectItem,
history,
@ -498,8 +499,7 @@ const SimpleFilesRow = (props) => {
}
setTooltipPosition(e.pageX, e.pageY);
document.body.classList.add("drag-cursor");
setDragging(true);
setStartDrag(true);
};
const isMobile = sectionWidth < 500;
@ -588,6 +588,7 @@ export default inject(
fileActionStore,
dragging,
setDragging,
setStartDrag,
setTooltipPosition,
} = filesStore;
@ -656,6 +657,7 @@ export default inject(
setMediaViewerData,
selectedFolderId,
setDragging,
setStartDrag,
startUpload,
onSelectItem,
setTooltipPosition,

View File

@ -21,6 +21,8 @@ const SectionBodyContent = (props) => {
folderId,
dragging,
setDragging,
startDrag,
setStartDrag,
setTooltipPosition,
isRecycleBinFolder,
moveDragItems,
@ -35,8 +37,8 @@ const SectionBodyContent = (props) => {
customScrollElm && customScrollElm.scrollTo(0, 0);
}
dragging && window.addEventListener("mouseup", onMouseUp);
dragging && document.addEventListener("mousemove", onMouseMove);
startDrag && window.addEventListener("mouseup", onMouseUp);
startDrag && document.addEventListener("mousemove", onMouseMove);
document.addEventListener("dragover", onDragOver);
document.addEventListener("dragleave", onDragLeaveDoc);
@ -49,10 +51,13 @@ const SectionBodyContent = (props) => {
document.removeEventListener("dragleave", onDragLeaveDoc);
document.removeEventListener("drop", onDropEvent);
};
}, [onMouseUp, onMouseMove, dragging, folderId]);
}, [onMouseUp, onMouseMove, startDrag, folderId]);
const onMouseMove = (e) => {
!dragging && setDragging(true);
if (!dragging) {
document.body.classList.add("drag-cursor");
setDragging(true);
}
setTooltipPosition(e.pageX, e.pageY);
@ -95,11 +100,14 @@ const SectionBodyContent = (props) => {
const elem = e.target.closest(".droppable");
const value = elem && elem.getAttribute("value");
if ((!value && !treeValue) || isRecycleBinFolder) {
return setDragging(false);
setDragging(false);
setStartDrag(false);
return;
}
const folderId = value ? value.split("_")[1] : treeValue;
setStartDrag(false);
setDragging(false);
onMoveTo(folderId);
return;
@ -156,6 +164,8 @@ export default inject(
filesList,
dragging,
setDragging,
startDrag,
setStartDrag,
isLoading,
viewAs,
setTooltipPosition,
@ -169,6 +179,8 @@ export default inject(
isLoading,
isEmptyFilesList: filesList.length <= 0,
setDragging,
startDrag,
setStartDrag,
folderId: selectedFolderStore.id,
setTooltipPosition,
isRecycleBinFolder: treeFoldersStore.isRecycleBinFolder,

View File

@ -33,6 +33,7 @@ class FilesStore {
tooltipPageX = 0;
tooltipPageY = 0;
startDrag = false;
firstLoad = true;
files = [];
@ -85,6 +86,10 @@ class FilesStore {
this.tooltipPageY = tooltipPageY;
};
setStartDrag = (startDrag) => {
this.startDrag = startDrag;
};
get tooltipValue() {
if (!this.dragging) return null;