From 91f3bad92e7fa4a234ad0fad95ea9ccd2cb7d88c Mon Sep 17 00:00:00 2001 From: nikec <43032218+niikeec@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:31:25 +0200 Subject: [PATCH] [ENG-1106] Rename text box improvements (#1348) rename text box improvements Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com> --- .../Explorer/FilePath/RenameTextBox.tsx | 201 +++++++++--------- .../app/$libraryId/Explorer/View/GridView.tsx | 4 +- .../app/$libraryId/Explorer/View/ListView.tsx | 21 +- 3 files changed, 115 insertions(+), 111 deletions(-) diff --git a/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx b/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx index 1bccdb5de..c647875dc 100644 --- a/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/RenameTextBox.tsx @@ -11,8 +11,8 @@ import { import { useKey } from 'rooks'; import { useLibraryMutation, useRspcLibraryContext } from '@sd/client'; import { toast, Tooltip } from '@sd/ui'; - import { useIsTextTruncated, useOperatingSystem } from '~/hooks'; + import { useExplorerViewContext } from '../ViewContext'; type Props = ComponentProps<'div'> & { @@ -33,135 +33,125 @@ export const RenameTextBoxBase = forwardRef( const os = useOperatingSystem(); const [allowRename, setAllowRename] = useState(false); - const [renamable, setRenamable] = useState(false); + + const renamable = useRef(false); + const timeout = useRef(null); const ref = useRef(null); useImperativeHandle(_ref, () => ref.current); + //this is to determine if file name is truncated + const isTruncated = useIsTextTruncated(ref, text); + // Highlight file name up to extension or // fully if it's a directory or has no extension const highlightText = useCallback(() => { - if (ref?.current) { - const range = document.createRange(); - const node = ref.current.firstChild; - if (!node) return; + if (!ref.current || !text) return; - const endRange = text?.lastIndexOf('.'); + const node = ref.current.firstChild; + if (!node) return; - range.setStart(node, 0); - range.setEnd(node, endRange && endRange !== -1 ? endRange : text?.length || 0); + const endRange = text.lastIndexOf('.'); - const sel = window.getSelection(); - sel?.removeAllRanges(); - sel?.addRange(range); - } + const range = document.createRange(); + + range.setStart(node, 0); + range.setEnd(node, endRange !== -1 ? endRange : text.length); + + const sel = window.getSelection(); + if (!sel) return; + + sel.removeAllRanges(); + sel.addRange(range); }, [text]); // Blur field - function blur() { - if (ref?.current) { - ref.current.blur(); - setAllowRename(false); - } - } + const blur = useCallback(() => ref.current?.blur(), []); // Reset to original file name - function reset() { - if (ref?.current) { - ref.current.innerText = text || ''; + const reset = () => ref.current && (ref.current.innerText = text ?? ''); + + const handleRename = async () => { + const newName = ref.current?.innerText.trim(); + + if (!newName || newName === text) { + reset(); + return; } - } - - async function handleRename() { - if (!ref?.current) return; - - const newName = ref?.current.innerText.trim(); - if (!(newName && locationId)) return reset(); - - const oldName = text; - if (!oldName || newName === oldName) return; await renameHandler(newName); - } + }; - // Handle keydown events - function handleKeyDown(e: React.KeyboardEvent) { + const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { - case 'Tab': + case 'Tab': { e.preventDefault(); blur(); break; - case 'Escape': + } + + case 'Escape': { + e.stopPropagation(); reset(); blur(); break; - case 'z': + } + + case 'z': { if (os === 'macOS' ? e.metaKey : e.ctrlKey) { reset(); highlightText(); } - break; - } - } - - //this is to determine if file name is truncated - const isTruncated = useIsTextTruncated(ref, text); - - // Focus and highlight when renaming is allowed - useEffect(() => { - if (allowRename) { - setTimeout(() => { - if (ref?.current) { - ref.current.focus(); - highlightText(); - } - }); - } - }, [allowRename, explorerView, highlightText]); - - // Handle renaming when triggered from outside - useEffect(() => { - if (!disabled) { - if (explorerView.isRenaming && !allowRename) setAllowRename(true); - else if (!explorerView.isRenaming && allowRename) setAllowRename(false); - } - }, [explorerView.isRenaming, disabled, allowRename]); - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (ref?.current && !ref.current.contains(event.target as Node)) { - blur(); } } + }; - document.addEventListener('mousedown', handleClickOutside, true); - return () => { - document.removeEventListener('mousedown', handleClickOutside, true); - }; - }, [ref]); - - // Rename or blur on Enter key - useKey('Enter', (e) => { - if (allowRename) blur(); - else if (!disabled) { - e.preventDefault(); - setAllowRename(true); - explorerView.setIsRenaming(true); + const resetState = () => { + setAllowRename(false); + renamable.current = false; + if (timeout.current) { + clearTimeout(timeout.current); + timeout.current = null; } + }; + + useKey('Enter', (e) => { + e.preventDefault(); + + if (allowRename) blur(); + else if (!disabled) setAllowRename(true); }); useEffect(() => { - const elem = ref.current; + const element = ref.current; + if (!element || !allowRename) return; + const scroll = (e: WheelEvent) => { - if (allowRename) { - e.preventDefault(); - if (elem) elem.scrollTop += e.deltaY; - } + e.preventDefault(); + element.scrollTop += e.deltaY; }; - elem?.addEventListener('wheel', scroll); - return () => elem?.removeEventListener('wheel', scroll); - }, [allowRename]); + highlightText(); + + element.addEventListener('wheel', scroll); + return () => element.removeEventListener('wheel', scroll); + }, [allowRename, highlightText]); + + useEffect(() => { + if (!disabled) { + if (explorerView.isRenaming && !allowRename) setAllowRename(true); + else explorerView.setIsRenaming(allowRename); + } else resetState(); + }, [explorerView.isRenaming, disabled, allowRename, explorerView]); + + useEffect(() => { + const onMouseDown = (event: MouseEvent) => { + if (!ref.current?.contains(event.target as Node)) blur(); + }; + + document.addEventListener('mousedown', onMouseDown, true); + return () => document.removeEventListener('mousedown', onMouseDown, true); + }, [blur]); return ( @@ -171,27 +161,29 @@ export const RenameTextBoxBase = forwardRef( contentEditable={allowRename} suppressContentEditableWarning className={clsx( - 'cursor-default truncate rounded-md px-1.5 py-px text-xs text-ink', + 'cursor-default truncate rounded-md px-1.5 py-px text-xs text-ink outline-none', allowRename && [ - 'whitespace-normal bg-app outline-none ring-2 ring-accent-deep', + 'whitespace-normal bg-app ring-2 ring-accent-deep', activeClassName ], className )} - onDoubleClick={(e) => e.stopPropagation()} - onMouseDown={(e) => e.button === 0 && setRenamable(!disabled)} + onDoubleClick={(e) => { + if (allowRename) e.stopPropagation(); + renamable.current = false; + }} + onMouseDownCapture={(e) => e.button === 0 && (renamable.current = !disabled)} onMouseUp={(e) => { - if (e.button === 0) { - if (renamable) { - setAllowRename(true); - explorerView.setIsRenaming(true); - } - setRenamable(false); + if (e.button === 0 || renamable.current || !allowRename) { + timeout.current = setTimeout( + () => renamable.current && setAllowRename(true), + 350 + ); } }} - onBlur={async () => { - await handleRename(); - setAllowRename(false); + onBlur={() => { + handleRename(); + resetState(); explorerView.setIsRenaming(false); }} onKeyDown={handleKeyDown} @@ -219,7 +211,7 @@ export const RenamePathTextBox = ({ // Reset to original file name function reset() { if (ref?.current) { - ref.current.innerText = props.text || ''; + ref.current.innerText = fileName ?? ''; } } @@ -227,7 +219,6 @@ export const RenamePathTextBox = ({ // Handle renaming async function rename(newName: string) { - // TODO: Warn user on rename fails if (!props.locationId || !props.itemId || newName === fileName) { reset(); return; diff --git a/interface/app/$libraryId/Explorer/View/GridView.tsx b/interface/app/$libraryId/Explorer/View/GridView.tsx index 2bdc87959..a639ca232 100644 --- a/interface/app/$libraryId/Explorer/View/GridView.tsx +++ b/interface/app/$libraryId/Explorer/View/GridView.tsx @@ -79,7 +79,9 @@ export default () => { selected={selected} cut={cut} isRenaming={explorerView.isRenaming} - renamable={explorer.selectedItems.size === 1 && !quickPreviewStore.open} + renamable={ + selected && explorer.selectedItems.size === 1 && !quickPreviewStore.open + } /> )} diff --git a/interface/app/$libraryId/Explorer/View/ListView.tsx b/interface/app/$libraryId/Explorer/View/ListView.tsx index 3d374d662..365d58c94 100644 --- a/interface/app/$libraryId/Explorer/View/ListView.tsx +++ b/interface/app/$libraryId/Explorer/View/ListView.tsx @@ -33,7 +33,7 @@ import { useLayoutContext } from '../../Layout/Context'; import { useExplorerContext } from '../Context'; import { FileThumb } from '../FilePath/Thumb'; import { InfoPill } from '../Inspector'; -import { getQuickPreviewStore } from '../QuickPreview/store'; +import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store'; import { createOrdering, getOrderingDirection, @@ -96,11 +96,13 @@ const HeaderColumnName = ({ name }: { name: string }) => { type Range = [string, string]; export default () => { + const layout = useLayoutContext(); const explorer = useExplorerContext(); const explorerStore = useExplorerStore(); - const settings = explorer.useSettingsSnapshot(); const explorerView = useExplorerViewContext(); - const layout = useLayoutContext(); + const settings = explorer.useSettingsSnapshot(); + + const quickPreviewStore = useQuickPreviewStore(); const tableRef = useRef(null); const tableHeaderRef = useRef(null); @@ -183,7 +185,11 @@ export default () => { allowHighlight={false} item={item} selected={selected} - disabled={!selected || explorer.selectedItems.size > 1} + disabled={ + !selected || + explorer.selectedItems.size > 1 || + quickPreviewStore.open + } style={{ maxHeight: 36 }} /> @@ -263,7 +269,12 @@ export default () => { } } ], - [explorer.selectedItems, settings.colSizes, explorerStore.cutCopyState] + [ + settings.colSizes, + explorer.selectedItems, + explorerStore.cutCopyState, + quickPreviewStore.open + ] ); const table = useReactTable({