mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-07 09:51:26 +00:00
Contextmenu improve (#3238)
* add a commen contextmenu component * optimized translate for menu * repair contextmenu bug * optimized share btn show code * repair showShareBtn bug * optimized contextmenu * optimized contextmenu code * complete dirent-item-menu logic * optimized contextmenu code * complete dirent-container-menu logic * complete tree-node-contextmenu logic * delete unnecessary code * optimized contextmenu func * repair bug * optimized code style * optimized code style * add a dirent-none-view for dir-list-view mode * optimized dirent-container-menu&dirent-item-menu * add select-item contextmenu * repair rebase bug
This commit is contained in:
33
frontend/src/components/context-menu/actions.js
Normal file
33
frontend/src/components/context-menu/actions.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import assign from 'object-assign';
|
||||
|
||||
import { store } from './helpers';
|
||||
|
||||
export const MENU_SHOW = 'REACT_CONTEXTMENU_SHOW';
|
||||
export const MENU_HIDE = 'REACT_CONTEXTMENU_HIDE';
|
||||
|
||||
|
||||
export function dispatchGlobalEvent(eventName, opts, target = window) {
|
||||
// Compatibale with IE
|
||||
// @see http://stackoverflow.com/questions/26596123/internet-explorer-9-10-11-event-constructor-doesnt-work
|
||||
let event;
|
||||
|
||||
if (typeof window.CustomEvent === 'function') {
|
||||
event = new window.CustomEvent(eventName, { detail: opts });
|
||||
} else {
|
||||
event = document.createEvent('CustomEvent');
|
||||
event.initCustomEvent(eventName, false, true, opts);
|
||||
}
|
||||
|
||||
if (target) {
|
||||
target.dispatchEvent(event);
|
||||
assign(store, opts);
|
||||
}
|
||||
}
|
||||
|
||||
export function showMenu(opts = {}, target) {
|
||||
dispatchGlobalEvent(MENU_SHOW, assign({}, opts, { type: MENU_SHOW }), target);
|
||||
}
|
||||
|
||||
export function hideMenu(opts = {}, target) {
|
||||
dispatchGlobalEvent(MENU_HIDE, assign({}, opts, { type: MENU_HIDE }), target);
|
||||
}
|
227
frontend/src/components/context-menu/context-menu.js
Normal file
227
frontend/src/components/context-menu/context-menu.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import listener from './globalEventListener';
|
||||
import { hideMenu } from './actions';
|
||||
import { callIfExists } from './helpers';
|
||||
|
||||
const propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
rtl: PropTypes.bool,
|
||||
onMenuItemClick: PropTypes.func.isRequired,
|
||||
onShowMenu: PropTypes.func,
|
||||
onHideMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
class ContextMenu extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isVisible: false,
|
||||
currentObject: null,
|
||||
menuList: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listenId = listener.register(this.handleShow, this.handleHide);
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
if (this.state.isVisible) {
|
||||
const wrapper = window.requestAnimationFrame || setTimeout;
|
||||
|
||||
wrapper(() => {
|
||||
const { x, y } = this.state;
|
||||
const { top, left } = this.props.rtl ? this.getRTLMenuPosition(x, y) : this.getMenuPosition(x, y);
|
||||
|
||||
wrapper(() => {
|
||||
if (!this.menu) return;
|
||||
this.menu.style.top = `${top}px`;
|
||||
this.menu.style.left = `${left}px`;
|
||||
this.menu.style.opacity = 1;
|
||||
this.menu.style.pointerEvents = 'auto';
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (!this.menu) return;
|
||||
this.menu.style.opacity = 0;
|
||||
this.menu.style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listenId) {
|
||||
listener.unregister(this.listenId);
|
||||
}
|
||||
|
||||
this.unregisterHandlers();
|
||||
}
|
||||
|
||||
registerHandlers = () => {
|
||||
document.addEventListener('mousedown', this.handleOutsideClick);
|
||||
document.addEventListener('touchstart', this.handleOutsideClick);
|
||||
document.addEventListener('scroll', this.handleHide);
|
||||
document.addEventListener('contextmenu', this.handleHide);
|
||||
document.addEventListener('keydown', this.handleKeyNavigation);
|
||||
window.addEventListener('resize', this.handleHide);
|
||||
}
|
||||
|
||||
unregisterHandlers = () => {
|
||||
document.removeEventListener('mousedown', this.handleOutsideClick);
|
||||
document.removeEventListener('touchstart', this.handleOutsideClick);
|
||||
document.removeEventListener('scroll', this.handleHide);
|
||||
document.removeEventListener('contextmenu', this.handleHide);
|
||||
document.removeEventListener('keydown', this.handleKeyNavigation);
|
||||
window.removeEventListener('resize', this.handleHide);
|
||||
}
|
||||
|
||||
handleShow = (e) => {
|
||||
if (e.detail.id !== this.props.id || this.state.isVisible) return;
|
||||
|
||||
const { x, y } = e.detail.position;
|
||||
const { currentObject, menuList} = e.detail;
|
||||
|
||||
this.setState({ isVisible: true, x, y, currentObject, menuList });
|
||||
this.registerHandlers();
|
||||
callIfExists(this.props.onShowMenu, e);
|
||||
}
|
||||
|
||||
handleHide = (e) => {
|
||||
if (this.state.isVisible && (!e.detail || !e.detail.id || e.detail.id === this.props.id)) {
|
||||
this.unregisterHandlers();
|
||||
this.setState({ isVisible: false});
|
||||
callIfExists(this.props.onHideMenu, e);
|
||||
}
|
||||
}
|
||||
|
||||
handleOutsideClick = (e) => {
|
||||
if (!this.menu.contains(e.target)) hideMenu();
|
||||
}
|
||||
|
||||
handleMouseLeave = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.props.hideOnLeave) hideMenu();
|
||||
}
|
||||
|
||||
handleContextMenu = (e) => {
|
||||
this.handleHide(e);
|
||||
}
|
||||
|
||||
handleKeyNavigation = (e) => {
|
||||
if (this.state.isVisible === false) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
this.hideMenu(e);
|
||||
}
|
||||
|
||||
hideMenu = (e) => {
|
||||
if (e.keyCode === 27 || e.keyCode === 13) { // ECS or enter
|
||||
hideMenu();
|
||||
}
|
||||
}
|
||||
|
||||
getMenuPosition = (x = 0, y = 0) => {
|
||||
let menuStyles = {
|
||||
top: y,
|
||||
left: x
|
||||
};
|
||||
|
||||
if (!this.menu) return menuStyles;
|
||||
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const rect = this.menu.getBoundingClientRect();
|
||||
|
||||
if (y + rect.height > innerHeight) {
|
||||
menuStyles.top -= rect.height;
|
||||
}
|
||||
|
||||
if (x + rect.width > innerWidth) {
|
||||
menuStyles.left -= rect.width;
|
||||
}
|
||||
|
||||
if (menuStyles.top < 0) {
|
||||
menuStyles.top = rect.height < innerHeight ? (innerHeight - rect.height) / 2 : 0;
|
||||
}
|
||||
|
||||
if (menuStyles.left < 0) {
|
||||
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
|
||||
}
|
||||
|
||||
return menuStyles;
|
||||
}
|
||||
|
||||
getRTLMenuPosition = (x = 0, y = 0) => {
|
||||
let menuStyles = {
|
||||
top: y,
|
||||
left: x
|
||||
};
|
||||
|
||||
if (!this.menu) return menuStyles;
|
||||
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const rect = this.menu.getBoundingClientRect();
|
||||
|
||||
// Try to position the menu on the left side of the cursor
|
||||
menuStyles.left = x - rect.width;
|
||||
|
||||
if (y + rect.height > innerHeight) {
|
||||
menuStyles.top -= rect.height;
|
||||
}
|
||||
|
||||
if (menuStyles.left < 0) {
|
||||
menuStyles.left += rect.width;
|
||||
}
|
||||
|
||||
if (menuStyles.top < 0) {
|
||||
menuStyles.top = rect.height < innerHeight ? (innerHeight - rect.height) / 2 : 0;
|
||||
}
|
||||
|
||||
if (menuStyles.left + rect.width > innerWidth) {
|
||||
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
|
||||
}
|
||||
|
||||
return menuStyles;
|
||||
}
|
||||
|
||||
|
||||
onMenuItemClick = (event) => {
|
||||
event.stopPropagation();
|
||||
let operation = event.target.dataset.operation;
|
||||
let currentObject = this.state.currentObject;
|
||||
this.props.onMenuItemClick(operation, currentObject, event);
|
||||
}
|
||||
|
||||
render() {
|
||||
const inlineStyle = { position: 'fixed', opacity: 0, pointerEvents: 'none', display: 'block' };
|
||||
return (
|
||||
<div role="menu" className="seafile-contextmenu dropdown-menu" style={inlineStyle} ref={menu => { this.menu = menu; }}>
|
||||
{this.state.menuList.map((menuItem, index) => {
|
||||
if (menuItem === 'Divider') {
|
||||
return <div key={index} className="seafile-divider dropdown-divider"></div>
|
||||
} else {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className="seafile-contextmenu-item dropdown-item"
|
||||
data-operation={menuItem.key}
|
||||
onClick={this.onMenuItemClick}
|
||||
>
|
||||
{menuItem.value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenu.propTypes = propTypes;
|
||||
|
||||
export default ContextMenu;
|
45
frontend/src/components/context-menu/globalEventListener.js
Normal file
45
frontend/src/components/context-menu/globalEventListener.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { MENU_SHOW, MENU_HIDE } from './actions';
|
||||
import { uniqueId, hasOwnProp, canUseDOM } from './helpers';
|
||||
|
||||
class GlobalEventListener {
|
||||
|
||||
constructor() {
|
||||
this.callbacks = {};
|
||||
|
||||
if (canUseDOM) {
|
||||
window.addEventListener(MENU_SHOW, this.handleShowEvent);
|
||||
window.addEventListener(MENU_HIDE, this.handleHideEvent);
|
||||
}
|
||||
}
|
||||
|
||||
handleShowEvent = (event) => {
|
||||
for (const id in this.callbacks) {
|
||||
if (hasOwnProp(this.callbacks, id)) this.callbacks[id].show(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleHideEvent = (event) => {
|
||||
for (const id in this.callbacks) {
|
||||
if (hasOwnProp(this.callbacks, id)) this.callbacks[id].hide(event);
|
||||
}
|
||||
}
|
||||
|
||||
register = (showCallback, hideCallback) => {
|
||||
const id = uniqueId();
|
||||
|
||||
this.callbacks[id] = {
|
||||
show: showCallback,
|
||||
hide: hideCallback
|
||||
};
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
unregister = (id) => {
|
||||
if (id && this.callbacks[id]) {
|
||||
delete this.callbacks[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new GlobalEventListener();
|
17
frontend/src/components/context-menu/helpers.js
Normal file
17
frontend/src/components/context-menu/helpers.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export function callIfExists(func, ...args) {
|
||||
return (typeof func === 'function') && func(...args);
|
||||
}
|
||||
|
||||
export function hasOwnProp(obj, prop) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, prop);
|
||||
}
|
||||
|
||||
export function uniqueId() {
|
||||
return Math.random().toString(36).substring(7);
|
||||
}
|
||||
|
||||
export const store = {};
|
||||
|
||||
export const canUseDOM = Boolean(
|
||||
typeof window !== 'undefined' && window.document && window.document.createElement
|
||||
);
|
Reference in New Issue
Block a user