From 9c537b62865d07d6ba7d9c8b8b84b2bea479db12 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 25 May 2023 17:48:55 +0200 Subject: [PATCH] implement click outside post menu to close --- src/components/views/Board.jsx | 43 +++- src/components/views/Catalog.jsx | 31 ++- src/components/views/Thread.jsx | 346 +++++++++++++++++-------------- 3 files changed, 258 insertions(+), 162 deletions(-) diff --git a/src/components/views/Board.jsx b/src/components/views/Board.jsx index bf6fb50e..cbe48770 100644 --- a/src/components/views/Board.jsx +++ b/src/components/views/Board.jsx @@ -68,6 +68,8 @@ const Board = () => { const linkRef = useRef(); const threadMenuRefs = useRef({}); const replyMenuRefs = useRef({}); + const postMenuRef = useRef(null); + const postMenuCatalogRef = useRef(null); const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'}); const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress}); @@ -111,6 +113,25 @@ const Board = () => { const handleOptionClick = () => { setOpenMenuCid(null); }; + + const handleOutsideClick = (e) => { + if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) { + setOpenMenuCid(null); + } + }; + + useEffect(() => { + if (openMenuCid !== null) { + document.addEventListener('click', handleOutsideClick); + } else { + document.removeEventListener('click', handleOutsideClick); + } + + return () => { + document.removeEventListener('click', handleOutsideClick); + }; + }, [openMenuCid]); + const errorString = useMemo(() => { @@ -829,10 +850,14 @@ const Board = () => { threadMenuRefs.current[thread.cid] = el} + ref={el => { + threadMenuRefs.current[thread.cid] = el; + postMenuRef.current = el; + }} className='post-menu-button' rotated={openMenuCid === thread.cid} - onClick={() => { + onClick={(event) => { + event.stopPropagation(); const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect(); setMenuPosition({top: rect.top + window.scrollY, left: rect.left}); setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid)); @@ -842,6 +867,8 @@ const Board = () => { {createPortal( {postMenuCatalogRef.current = el}} + onClick={(event) => event.stopPropagation()} style={{position: "absolute", top: menuPosition.top + 7, left: menuPosition.left}}> @@ -1029,19 +1056,25 @@ const Board = () => { replyMenuRefs.current[reply.cid] = el} + ref={el => { + replyMenuRefs.current[reply.cid] = el; + postMenuRef.current = el; + }} className='post-menu-button' rotated={openMenuCid === reply.cid} - onClick={() => { + onClick={(event) => { + event.stopPropagation(); const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect(); setMenuPosition({top: rect.top + window.scrollY, left: rect.left}); setOpenMenuCid(prevCid => (prevCid === reply.cid ? null : reply.cid)); - }} + }} > ▶ {createPortal( {postMenuCatalogRef.current = el}} + onClick={(event) => event.stopPropagation()} style={{position: "absolute", top: menuPosition.top + 7, left: menuPosition.left}}> diff --git a/src/components/views/Catalog.jsx b/src/components/views/Catalog.jsx index 7a9067c0..96d3bd0b 100644 --- a/src/components/views/Catalog.jsx +++ b/src/components/views/Catalog.jsx @@ -53,6 +53,8 @@ const Catalog = () => { const commentRef = useRef(); const linkRef = useRef(); const threadMenuRefs = useRef({}); + const postMenuRef = useRef(null); + const postMenuCatalogRef = useRef(null); const navigate = useNavigate(); @@ -101,6 +103,25 @@ const Catalog = () => { setOpenMenuCid(null); }; + const handleOutsideClick = (e) => { + if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) { + setOpenMenuCid(null); + } + }; + + useEffect(() => { + if (openMenuCid !== null) { + document.addEventListener('click', handleOutsideClick); + } else { + document.removeEventListener('click', handleOutsideClick); + } + + return () => { + document.removeEventListener('click', handleOutsideClick); + }; + }, [openMenuCid]); + + useEffect(() => { setSelectedAddress(subplebbitAddress); @@ -683,11 +704,15 @@ const Catalog = () => { zIndex: '999'}} key={`pmb-${index}`} title="Post menu" - ref={el => threadMenuRefs.current[thread.cid] = el} + ref={el => { + threadMenuRefs.current[thread.cid] = el; + postMenuRef.current = el; + }} className='post-menu-button' id='post-menu-button-catalog' rotated={openMenuCid === thread.cid} - onClick={() => { + onClick={(event) => { + event.stopPropagation(); const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect(); setMenuPosition({top: rect.top + window.scrollY, left: rect.left}); setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid)); @@ -698,6 +723,8 @@ const Catalog = () => { {createPortal( {postMenuCatalogRef.current = el}} + onClick={(event) => event.stopPropagation()} style={{position: "absolute", top: menuPosition.top + 7, left: menuPosition.left}}> diff --git a/src/components/views/Thread.jsx b/src/components/views/Thread.jsx index 9c0ad93b..f6646f04 100644 --- a/src/components/views/Thread.jsx +++ b/src/components/views/Thread.jsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { confirmAlert } from 'react-confirm-alert'; @@ -9,6 +10,7 @@ import { debounce } from 'lodash'; import useGeneralStore from '../../hooks/stores/useGeneralStore'; import { Container, NavBar, Header, Break, PostForm, PostFormTable, PostMenu } from '../styled/views/Board.styled'; import { ReplyFormLink, TopBar, BottomBar, BoardForm, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled'; +import { PostMenuCatalog } from '../styled/views/Catalog.styled'; import EditModal from '../modals/EditModal'; import ImageBanner from '../ImageBanner'; import ModerationModal from '../modals/ModerationModal'; @@ -64,6 +66,8 @@ const Thread = () => { const linkRef = useRef(); const threadMenuRefs = useRef({}); const replyMenuRefs = useRef({}); + const postMenuRef = useRef(null); + const postMenuCatalogRef = useRef(null); const [triggerPublishComment, setTriggerPublishComment] = useState(false); const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); @@ -75,10 +79,11 @@ const Thread = () => { const [originalCommentContent, setOriginalCommentContent] = useState(null); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); - const [rotatedStates, setRotatedStates] = useState({}); const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); const [isModerator, setIsModerator] = useState(false); const [commentCid, setCommentCid] = useState(null); + const [menuPosition, setMenuPosition] = useState({top: 0, left: 0}); + const [openMenuCid, setOpenMenuCid] = useState(null); useError(errorMessage, [errorMessage]); useSuccess(successMessage, [successMessage]); @@ -107,13 +112,28 @@ const Thread = () => { }, [account?.author.address, subplebbit.roles]); - const handleOptionClick = (threadCid) => { - setRotatedStates(prevState => ({ - ...prevState, - [threadCid]: false - })); + const handleOptionClick = () => { + setOpenMenuCid(null); }; + const handleOutsideClick = (e) => { + if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) { + setOpenMenuCid(null); + } + }; + + useEffect(() => { + if (openMenuCid !== null) { + document.addEventListener('click', handleOutsideClick); + } else { + document.removeEventListener('click', handleOutsideClick); + } + + return () => { + document.removeEventListener('click', handleOutsideClick); + }; + }, [openMenuCid]); + useEffect(() => { window.scrollTo(0, 0); @@ -790,91 +810,99 @@ const Thread = () => { threadMenuRefs.current[comment.cid] = el} + ref={el => { + threadMenuRefs.current[comment.cid] = el; + postMenuRef.current = el; + }} className='post-menu-button' - rotated={rotatedStates[comment.cid]} - onClick={() => { + rotated={openMenuCid === comment.cid} + onClick={(event) => { + event.stopPropagation(); const rect = threadMenuRefs.current[comment.cid].getBoundingClientRect(); - const menu = document.querySelector(`.post-menu-thread-${comment.cid}`); - menu.style.top = `calc(${rect.top + window.scrollY}px + 17px)`; - menu.style.left = `${rect.left}px`; - - setRotatedStates(prevState => ({ - ...prevState, - [comment.cid]: !prevState[comment.cid] - })); + setMenuPosition({top: rect.top + window.scrollY, left: rect.left}); + setOpenMenuCid(prevCid => (prevCid === comment.cid ? null : comment.cid)); }} > ▶ -
-
    -
  • handleOptionClick(comment.cid)}>Hide thread
  • - {comment?.author?.shortAddress === account?.author?.shortAddress ? ( - <> -
  • handleAuthorEditClick(comment)}>Edit post
  • -
  • handleAuthorDeleteClick(comment.cid)}>Delete post
  • - - ) : null} - {isModerator ? ( - <> - {comment?.author?.shortAddress === account?.author?.shortAddress ? ( - null - ) : ( -
  • { + {createPortal( + {postMenuCatalogRef.current = el}} + onClick={(event) => event.stopPropagation()} + style={{position: "absolute", + top: menuPosition.top + 7, + left: menuPosition.left}}> +
    +
      +
    • handleOptionClick(comment.cid)}>Hide thread
    • + {comment.author.shortAddress === account?.author.shortAddress ? ( + <> +
    • handleAuthorEditClick(comment)}>Edit post
    • +
    • handleAuthorDeleteClick(comment.cid)}>Delete post
    • + + ) : null} + {isModerator ? ( + <> + {comment.author.shortAddress === account?.author.shortAddress ? ( + null + ) : ( +
    • { + setModeratingCommentCid(comment.cid) + setIsModerationOpen(true); + handleOptionClick(comment.cid); + setDeletePost(true); + }}> + Delete post +
    • + )} +
    • { setModeratingCommentCid(comment.cid) setIsModerationOpen(true); handleOptionClick(comment.cid); - setDeletePost(true); }}> - Delete post + Mod tools
    • - )} -
    • { - setModeratingCommentCid(comment.cid) - setIsModerationOpen(true); - handleOptionClick(comment.cid); - }}> - Mod tools -
    • - - ) : null} - {(commentMediaInfo && ( - commentMediaInfo.type === 'image' || - (commentMediaInfo.type === 'webpage' && - commentMediaInfo.thumbnail))) ? ( -
    • {setIsImageSearchOpen(true)}} - onMouseLeave={() => {setIsImageSearchOpen(false)}}> - Image search » -
        -
      • handleOptionClick(comment.cid)}> - Google -
      • -
      • handleOptionClick(comment.cid)}> - Yandex -
      • -
      • handleOptionClick(comment.cid)}> - SauceNAO -
      • -
      -
    • - ) : null - } -
    -
    + + ) : null} + {(commentMediaInfo && ( + commentMediaInfo.type === 'image' || + (commentMediaInfo.type === 'webpage' && + commentMediaInfo.thumbnail))) ? ( +
  • {setIsImageSearchOpen(true)}} + onMouseLeave={() => {setIsImageSearchOpen(false)}}> + Image search » +
      +
    • handleOptionClick(comment.cid)}> + Google +
    • +
    • handleOptionClick(comment.cid)}> + Yandex +
    • +
    • handleOptionClick(comment.cid)}> + SauceNAO +
    • +
    +
  • + ) : null + } +
+
+
, document.body + )}