Skip to content

Commit fbaabc2

Browse files
authored
Merge branch 'develop' into fix/2163
2 parents 984deab + 34a46a3 commit fbaabc2

File tree

4 files changed

+92
-49
lines changed

4 files changed

+92
-49
lines changed
Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { createRef } from 'react';
22
import styled from 'styled-components/macro';
33
import tw from 'twin.macro';
44
import Fade from '@/components/elements/Fade';
@@ -17,64 +17,91 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
1717
}
1818
`;
1919

20-
const DropdownMenu = ({ renderToggle, children }: Props) => {
21-
const menu = useRef<HTMLDivElement>(null);
22-
const [ posX, setPosX ] = useState(0);
23-
const [ visible, setVisible ] = useState(false);
20+
interface State {
21+
posX: number;
22+
visible: boolean;
23+
}
2424

25-
const onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
26-
e.preventDefault();
25+
class DropdownMenu extends React.PureComponent<Props, State> {
26+
menu = createRef<HTMLDivElement>();
27+
28+
state: State = {
29+
posX: 0,
30+
visible: false,
31+
};
32+
33+
componentWillUnmount () {
34+
this.removeListeners();
35+
}
36+
37+
componentDidUpdate (prevProps: Readonly<Props>, prevState: Readonly<State>) {
38+
const menu = this.menu.current;
39+
40+
if (this.state.visible && !prevState.visible && menu) {
41+
document.addEventListener('click', this.windowListener);
42+
document.addEventListener('contextmenu', this.contextMenuListener);
43+
menu.setAttribute(
44+
'style', `left: ${Math.round(this.state.posX - menu.clientWidth)}px`,
45+
);
46+
}
47+
48+
if (!this.state.visible && prevState.visible) {
49+
this.removeListeners();
50+
}
51+
}
52+
53+
removeListeners = () => {
54+
document.removeEventListener('click', this.windowListener);
55+
document.removeEventListener('contextmenu', this.contextMenuListener);
56+
};
2757

28-
!visible && setPosX(e.clientX);
29-
setVisible(s => !s);
58+
onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
59+
e.preventDefault();
60+
this.triggerMenu(e.clientX);
3061
};
3162

32-
const windowListener = (e: MouseEvent) => {
33-
if (e.button === 2 || !visible || !menu.current) {
63+
contextMenuListener = () => this.setState({ visible: false });
64+
65+
windowListener = (e: MouseEvent) => {
66+
const menu = this.menu.current;
67+
68+
if (e.button === 2 || !this.state.visible || !menu) {
3469
return;
3570
}
3671

37-
if (e.target === menu.current || menu.current.contains(e.target as Node)) {
72+
if (e.target === menu || menu.contains(e.target as Node)) {
3873
return;
3974
}
4075

41-
if (e.target !== menu.current && !menu.current.contains(e.target as Node)) {
42-
setVisible(false);
76+
if (e.target !== menu && !menu.contains(e.target as Node)) {
77+
this.setState({ visible: false });
4378
}
4479
};
4580

46-
useEffect(() => {
47-
if (!visible || !menu.current) {
48-
return;
49-
}
81+
triggerMenu = (posX: number) => this.setState(s => ({
82+
posX: !s.visible ? posX : s.posX,
83+
visible: !s.visible,
84+
}));
5085

51-
document.addEventListener('click', windowListener);
52-
menu.current.setAttribute(
53-
'style', `left: ${Math.round(posX - menu.current.clientWidth)}px`,
86+
render () {
87+
return (
88+
<div>
89+
{this.props.renderToggle(this.onClickHandler)}
90+
<Fade timeout={150} in={this.state.visible} unmountOnExit>
91+
<div
92+
ref={this.menu}
93+
onClick={e => {
94+
e.stopPropagation();
95+
this.setState({ visible: false });
96+
}}
97+
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
98+
>
99+
{this.props.children}
100+
</div>
101+
</Fade>
102+
</div>
54103
);
55-
56-
return () => {
57-
document.removeEventListener('click', windowListener);
58-
};
59-
}, [ visible ]);
60-
61-
return (
62-
<div>
63-
{renderToggle(onClickHandler)}
64-
<Fade timeout={150} in={visible} unmountOnExit>
65-
<div
66-
ref={menu}
67-
onClick={e => {
68-
e.stopPropagation();
69-
setVisible(false);
70-
}}
71-
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
72-
>
73-
{children}
74-
</div>
75-
</Fade>
76-
</div>
77-
);
78-
};
104+
}
105+
}
79106

80107
export default DropdownMenu;

resources/scripts/components/server/files/FileDropdownMenu.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useRef, useState } from 'react';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import {
44
faCopy,
@@ -24,6 +24,7 @@ import { FileObject } from '@/api/server/files/loadDirectory';
2424
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
2525
import DropdownMenu from '@/components/elements/DropdownMenu';
2626
import styled from 'styled-components/macro';
27+
import useEventListener from '@/plugins/useEventListener';
2728

2829
type ModalType = 'rename' | 'move';
2930

@@ -46,15 +47,21 @@ const Row = ({ icon, title, ...props }: RowProps) => (
4647
);
4748

4849
export default ({ file }: { file: FileObject }) => {
50+
const onClickRef = useRef<DropdownMenu>(null);
4951
const [ showSpinner, setShowSpinner ] = useState(false);
5052
const [ modal, setModal ] = useState<ModalType | null>(null);
5153

5254
const { uuid } = useServer();
5355
const { mutate } = useFileManagerSwr();
5456
const { clearAndAddHttpError, clearFlashes } = useFlash();
55-
5657
const directory = ServerContext.useStoreState(state => state.files.directory);
5758

59+
useEventListener(`pterodactyl:files:ctx:${file.uuid}`, (e: CustomEvent) => {
60+
if (onClickRef.current) {
61+
onClickRef.current.triggerMenu(e.detail);
62+
}
63+
});
64+
5865
const doDeletion = () => {
5966
clearFlashes('files');
6067

@@ -95,6 +102,7 @@ export default ({ file }: { file: FileObject }) => {
95102

96103
return (
97104
<DropdownMenu
105+
ref={onClickRef}
98106
renderToggle={onClick => (
99107
<div css={tw`p-3 hover:text-white`} onClick={onClick}>
100108
<FontAwesomeIcon icon={faEllipsisH}/>
@@ -112,9 +120,11 @@ export default ({ file }: { file: FileObject }) => {
112120
<Row onClick={() => setModal('rename')} icon={faPencilAlt} title={'Rename'}/>
113121
<Row onClick={() => setModal('move')} icon={faLevelUpAlt} title={'Move'}/>
114122
</Can>
123+
{file.isFile &&
115124
<Can action={'file.create'}>
116125
<Row onClick={doCopy} icon={faCopy} title={'Copy'}/>
117126
</Can>
127+
}
118128
<Row onClick={doDownload} icon={faFileDownload} title={'Download'}/>
119129
<Can action={'file.delete'}>
120130
<Row onClick={doDeletion} icon={faTrashAlt} title={'Delete'} $danger/>

resources/scripts/components/server/files/FileObjectRow.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
3737
};
3838

3939
return (
40-
<Row key={file.name}>
40+
<Row
41+
key={file.name}
42+
onContextMenu={e => {
43+
e.preventDefault();
44+
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.uuid}`, { detail: e.clientX }));
45+
}}
46+
>
4147
<NavLink
4248
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`}
4349
css={tw`flex flex-1 text-neutral-300 no-underline p-3`}

resources/scripts/plugins/useEventListener.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useRef } from 'react';
22

3-
export default (eventName: string, handler: any, element: any = window) => {
3+
export default (eventName: string, handler: (e: Event | CustomEvent | UIEvent | any) => void, element: any = window) => {
44
const savedHandler = useRef<any>(null);
55

66
useEffect(() => {

0 commit comments

Comments
 (0)