mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-20 07:37:26 -05:00
* wip * wip 2 * Grid list single selection * core & pnpm-lock * Merge branch 'main' Conflicts: interface/app/$libraryId/Explorer/index.tsx * missing import from merge * fix total_orphan_paths bug * add top bar context * missing pieces of merge * missing pieces of merge * missing divs * Fill fallback value - was causing null error of page * spelling fixes * notice light theme, list view update, other explorer updates * Update pnpm-lock * Remove procedure * fix light menu ink color * fix list view scrolled state * Change layout default * Remove unused imports * remove keys * empty notice & context menu overview * Fix prevent selection while context menu is up * Fix scroll with keys * Empty notice icon * Add light icons * Context menu and fixed list view scroll * Fix name column sizing * top/bottom scroll position * Tag assign only when objectData * Fix list view locked state * fix ci * shamefully ignore eslint --------- Co-authored-by: Jamie Pine <ijamespine@me.com> Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com> Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com> Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com>
185 lines
4.2 KiB
TypeScript
185 lines
4.2 KiB
TypeScript
import clsx from 'clsx';
|
|
import { HTMLAttributes, useEffect, useRef, useState } from 'react';
|
|
import { useKey } from 'rooks';
|
|
import { FilePath, useLibraryMutation } from '@sd/client';
|
|
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
|
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
|
|
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
filePathData: FilePath;
|
|
selected: boolean;
|
|
activeClassName?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export default ({
|
|
filePathData,
|
|
selected,
|
|
className,
|
|
activeClassName,
|
|
disabled,
|
|
...props
|
|
}: Props) => {
|
|
const explorerStore = useExplorerStore();
|
|
const os = useOperatingSystem();
|
|
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const [allowRename, setAllowRename] = useState(false);
|
|
|
|
const renameFile = useLibraryMutation(['files.renameFile'], {
|
|
onError: () => reset()
|
|
});
|
|
|
|
const fileName = `${filePathData?.name}${
|
|
filePathData?.extension && `.${filePathData.extension}`
|
|
}`;
|
|
|
|
// Reset to original file name
|
|
function reset() {
|
|
if (ref.current) {
|
|
ref.current.innerText = fileName;
|
|
}
|
|
}
|
|
|
|
// Handle renaming
|
|
function rename() {
|
|
if (ref.current) {
|
|
const innerText = ref.current.innerText.trim();
|
|
if (!innerText) return reset();
|
|
|
|
const newName = innerText;
|
|
if (filePathData) {
|
|
const oldName =
|
|
filePathData.is_dir || !filePathData.extension
|
|
? filePathData.name
|
|
: filePathData.name + '.' + filePathData.extension;
|
|
|
|
if (newName !== oldName) {
|
|
renameFile.mutate({
|
|
location_id: filePathData.location_id,
|
|
file_name: oldName,
|
|
new_file_name: newName
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Highlight file name up to extension or
|
|
// fully if it's a directory or has no extension
|
|
function highlightFileName() {
|
|
if (ref.current) {
|
|
const range = document.createRange();
|
|
const node = ref.current.firstChild;
|
|
if (!node) return;
|
|
|
|
range.setStart(node, 0);
|
|
range.setEnd(node, filePathData?.name.length || 0);
|
|
|
|
const sel = window.getSelection();
|
|
sel?.removeAllRanges();
|
|
sel?.addRange(range);
|
|
}
|
|
}
|
|
|
|
// Blur field
|
|
function blur() {
|
|
if (ref.current) {
|
|
ref.current.blur();
|
|
setAllowRename(false);
|
|
}
|
|
}
|
|
|
|
// Handle keydown events
|
|
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
|
switch (e.key) {
|
|
case 'Tab':
|
|
e.preventDefault();
|
|
blur();
|
|
break;
|
|
case 'Escape':
|
|
reset();
|
|
blur();
|
|
break;
|
|
case 'z':
|
|
if (os === 'macOS' ? e.metaKey : e.ctrlKey) {
|
|
reset();
|
|
highlightFileName();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Focus and highlight when renaming is allowed
|
|
useEffect(() => {
|
|
if (allowRename) {
|
|
getExplorerStore().isRenaming = true;
|
|
setTimeout(() => {
|
|
if (ref.current) {
|
|
ref.current.focus();
|
|
highlightFileName();
|
|
}
|
|
});
|
|
} else getExplorerStore().isRenaming = false;
|
|
}, [allowRename]);
|
|
|
|
// Handle renaming when triggered from outside
|
|
useEffect(() => {
|
|
if (selected) {
|
|
if (explorerStore.isRenaming && !allowRename) setAllowRename(true);
|
|
else if (!explorerStore.isRenaming && allowRename) setAllowRename(false);
|
|
}
|
|
}, [explorerStore.isRenaming]);
|
|
|
|
// Rename or blur on Enter key
|
|
useKey('Enter', (e) => {
|
|
if (allowRename) {
|
|
e.preventDefault();
|
|
blur();
|
|
} else if (selected && !disabled) setAllowRename(true);
|
|
});
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
blur();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [ref]);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
role="textbox"
|
|
contentEditable={allowRename}
|
|
suppressContentEditableWarning
|
|
className={clsx(
|
|
'cursor-default overflow-y-auto truncate rounded-md px-1.5 py-px text-xs text-ink',
|
|
allowRename && [
|
|
'whitespace-normal bg-app outline-none ring-2 ring-accent-deep',
|
|
activeClassName
|
|
],
|
|
className
|
|
)}
|
|
onClick={(e) => {
|
|
if (selected || allowRename) e.stopPropagation();
|
|
if (selected && !disabled) setAllowRename(true);
|
|
}}
|
|
onBlur={() => {
|
|
rename();
|
|
setAllowRename(false);
|
|
}}
|
|
onKeyDown={handleKeyDown}
|
|
{...props}
|
|
>
|
|
{fileName}
|
|
</div>
|
|
);
|
|
};
|