From 117c1b1778dcf2a730e471667626be45329a35ae Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 11 Jul 2020 11:12:59 -0700 Subject: [PATCH] Support right click to use file context menu --- .../components/elements/DropdownMenu.tsx | 119 +++++++++++------- .../server/files/FileDropdownMenu.tsx | 14 ++- .../components/server/files/FileObjectRow.tsx | 8 +- resources/scripts/plugins/useEventListener.ts | 2 +- 4 files changed, 94 insertions(+), 49 deletions(-) diff --git a/resources/scripts/components/elements/DropdownMenu.tsx b/resources/scripts/components/elements/DropdownMenu.tsx index 05b3d3c7c..b32976e1c 100644 --- a/resources/scripts/components/elements/DropdownMenu.tsx +++ b/resources/scripts/components/elements/DropdownMenu.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { createRef } from 'react'; import styled from 'styled-components/macro'; import tw from 'twin.macro'; import Fade from '@/components/elements/Fade'; @@ -17,64 +17,93 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>` } `; -const DropdownMenu = ({ renderToggle, children }: Props) => { - const menu = useRef(null); - const [ posX, setPosX ] = useState(0); - const [ visible, setVisible ] = useState(false); +interface State { + posX: number; + visible: boolean; +} - const onClickHandler = (e: React.MouseEvent) => { +class DropdownMenu extends React.PureComponent { + menu = createRef(); + + state: State = { + posX: 0, + visible: false, + }; + + componentWillUnmount () { + this.removeListeners(); + } + + componentDidUpdate (prevProps: Readonly, prevState: Readonly) { + const menu = this.menu.current; + + if (this.state.visible && !prevState.visible && menu) { + document.addEventListener('click', this.windowListener); + document.addEventListener('contextmenu', this.contextMenuListener); + menu.setAttribute( + 'style', `left: ${Math.round(this.state.posX - menu.clientWidth)}px`, + ); + } + + if (!this.state.visible && prevState.visible) { + this.removeListeners(); + } + } + + removeListeners = () => { + document.removeEventListener('click', this.windowListener); + document.removeEventListener('contextmenu', this.contextMenuListener); + }; + + onClickHandler = (e: React.MouseEvent) => { e.preventDefault(); - - !visible && setPosX(e.clientX); - setVisible(s => !s); + this.triggerMenu(e.clientX); }; - const windowListener = (e: MouseEvent) => { - if (e.button === 2 || !visible || !menu.current) { + contextMenuListener = () => this.setState({ visible: false }); + + windowListener = (e: MouseEvent) => { + const menu = this.menu.current; + + console.log('windowListener:', e.button); + + if (e.button === 2 || !this.state.visible || !menu) { return; } - if (e.target === menu.current || menu.current.contains(e.target as Node)) { + if (e.target === menu || menu.contains(e.target as Node)) { return; } - if (e.target !== menu.current && !menu.current.contains(e.target as Node)) { - setVisible(false); + if (e.target !== menu && !menu.contains(e.target as Node)) { + this.setState({ visible: false }); } }; - useEffect(() => { - if (!visible || !menu.current) { - return; - } + triggerMenu = (posX: number) => this.setState(s => ({ + posX: !s.visible ? posX : s.posX, + visible: !s.visible, + })); - document.addEventListener('click', windowListener); - menu.current.setAttribute( - 'style', `left: ${Math.round(posX - menu.current.clientWidth)}px`, + render () { + return ( +
+ {this.props.renderToggle(this.onClickHandler)} + +
{ + e.stopPropagation(); + this.setState({ visible: false }); + }} + css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`} + > + {this.props.children} +
+
+
); - - return () => { - document.removeEventListener('click', windowListener); - }; - }, [ visible ]); - - return ( -
- {renderToggle(onClickHandler)} - -
{ - e.stopPropagation(); - setVisible(false); - }} - css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`} - > - {children} -
-
-
- ); -}; + } +} export default DropdownMenu; diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index 443d50a82..c03e5396e 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCopy, @@ -24,6 +24,7 @@ import { FileObject } from '@/api/server/files/loadDirectory'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; import DropdownMenu from '@/components/elements/DropdownMenu'; import styled from 'styled-components/macro'; +import useEventListener from '@/plugins/useEventListener'; type ModalType = 'rename' | 'move'; @@ -46,15 +47,21 @@ const Row = ({ icon, title, ...props }: RowProps) => ( ); export default ({ file }: { file: FileObject }) => { + const onClickRef = useRef(null); const [ showSpinner, setShowSpinner ] = useState(false); const [ modal, setModal ] = useState(null); const { uuid } = useServer(); const { mutate } = useFileManagerSwr(); const { clearAndAddHttpError, clearFlashes } = useFlash(); - const directory = ServerContext.useStoreState(state => state.files.directory); + useEventListener(`pterodactyl:files:ctx:${file.uuid}`, (e: CustomEvent) => { + if (onClickRef.current) { + onClickRef.current.triggerMenu(e.detail); + } + }); + const doDeletion = () => { clearFlashes('files'); @@ -95,6 +102,7 @@ export default ({ file }: { file: FileObject }) => { return ( (
@@ -112,9 +120,11 @@ export default ({ file }: { file: FileObject }) => { setModal('rename')} icon={faPencilAlt} title={'Rename'}/> setModal('move')} icon={faLevelUpAlt} title={'Move'}/> + {file.isFile && + } diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx index 66ed8ca09..2f097e1b9 100644 --- a/resources/scripts/components/server/files/FileObjectRow.tsx +++ b/resources/scripts/components/server/files/FileObjectRow.tsx @@ -37,7 +37,13 @@ const FileObjectRow = ({ file }: { file: FileObject }) => { }; return ( - + { + e.preventDefault(); + window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.uuid}`, { detail: e.clientX })); + }} + > { +export default (eventName: string, handler: (e: Event | CustomEvent | UIEvent | any) => void, element: any = window) => { const savedHandler = useRef(null); useEffect(() => {