mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-10-23 01:24:36 -04:00
[ENG-1106] Rename text box improvements (#1348)
rename text box improvements Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
1 parent
742dc2bb5a
commit
91f3bad92e
3 files changed
+115
-111
No files matched your search
@@ -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<HTMLDivElement | null, Props>(
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const [allowRename, setAllowRename] = useState(false);
|
||||
const [renamable, setRenamable] = useState(false);
|
||||
|
||||
const renamable = useRef<boolean>(false);
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(_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<HTMLDivElement>) {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
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 (
|
||||
<Tooltip label={!isTruncated || allowRename ? null : text} asChild>
|
||||
@@ -171,27 +161,29 @@ export const RenameTextBoxBase = forwardRef<HTMLDivElement | null, Props>(
|
||||
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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</GridList>
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const tableHeaderRef = useRef<HTMLDivElement>(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 }}
|
||||
/>
|
||||
</div>
|
||||
@@ -263,7 +269,12 @@ export default () => {
|
||||
}
|
||||
}
|
||||
],
|
||||
[explorer.selectedItems, settings.colSizes, explorerStore.cutCopyState]
|
||||
[
|
||||
settings.colSizes,
|
||||
explorer.selectedItems,
|
||||
explorerStore.cutCopyState,
|
||||
quickPreviewStore.open
|
||||
]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
|
||||
Reference in new issue
Block a user