adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
321 lines
6.6 KiB
Svelte
321 lines
6.6 KiB
Svelte
<script lang="ts">
|
|
import type { FileNode } from "./types";
|
|
import type { SelectData } from "@gradio/utils";
|
|
|
|
import Arrow from "./ArrowIcon.svelte";
|
|
import Checkbox from "./Checkbox.svelte";
|
|
import Self from "./FileTree.svelte";
|
|
import FileIcon from "../icons/light-file.svg";
|
|
import FolderIcon from "../icons/light-folder.svg";
|
|
|
|
interface Props {
|
|
path?: string[];
|
|
index_path?: number[];
|
|
selected_files?: string[][];
|
|
selected_folders?: string[][];
|
|
is_selected_entirely?: boolean;
|
|
interactive: boolean;
|
|
selectable?: boolean;
|
|
ls_fn: (path: string[]) => Promise<FileNode[]>;
|
|
file_count?: "single" | "multiple";
|
|
valid_for_selection: boolean;
|
|
oncheck?: (detail: {
|
|
path: string[];
|
|
checked: boolean;
|
|
type: "file" | "folder";
|
|
}) => void;
|
|
onselect?: (detail: SelectData) => void;
|
|
}
|
|
|
|
let {
|
|
path = [],
|
|
index_path = [],
|
|
selected_files = [],
|
|
selected_folders = [],
|
|
is_selected_entirely = false,
|
|
interactive,
|
|
selectable = false,
|
|
ls_fn,
|
|
file_count = "multiple",
|
|
valid_for_selection,
|
|
oncheck,
|
|
onselect
|
|
}: Props = $props();
|
|
|
|
let content = $state<FileNode[]>([]);
|
|
let opened_folders = $state<number[]>([]);
|
|
|
|
const toggle_open_folder = (i: number): void => {
|
|
if (opened_folders.includes(i)) {
|
|
opened_folders = opened_folders.filter((x) => x !== i);
|
|
} else {
|
|
opened_folders = [...opened_folders, i];
|
|
}
|
|
};
|
|
|
|
const open_folder = (i: number): void => {
|
|
if (!opened_folders.includes(i)) {
|
|
opened_folders = [...opened_folders, i];
|
|
}
|
|
};
|
|
|
|
(async () => {
|
|
content = await ls_fn(path);
|
|
if (valid_for_selection) {
|
|
content = [{ name: ".", type: "file" }, ...content];
|
|
}
|
|
opened_folders = content
|
|
.map((x, i) =>
|
|
x.type === "folder" &&
|
|
(is_selected_entirely || selected_files.some((y) => y[0] === x.name))
|
|
? i
|
|
: null
|
|
)
|
|
.filter((x): x is number => x !== null);
|
|
})();
|
|
|
|
$effect(() => {
|
|
if (is_selected_entirely) {
|
|
content.forEach((x) => {
|
|
oncheck?.({
|
|
path: [...path, x.name],
|
|
checked: true,
|
|
type: x.type
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
function handle_select(
|
|
full_index_path: number[],
|
|
item_path: string[],
|
|
_type: "file" | "folder"
|
|
): void {
|
|
onselect?.({
|
|
index: full_index_path as any,
|
|
value: item_path.join("/"),
|
|
selected: true
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<ul class:no-checkboxes={!interactive} class:root={path.length === 0}>
|
|
{#each content as { type, name, valid }, i}
|
|
{@const is_selected = (
|
|
type === "file" ? selected_files : selected_folders
|
|
).some((x) => x[0] === name && x.length === 1)}
|
|
<li>
|
|
<span
|
|
class="wrap"
|
|
class:selected={!interactive && is_selected}
|
|
class:selectable
|
|
role={selectable ? "button" : undefined}
|
|
tabindex={selectable ? 0 : undefined}
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
if (selectable) {
|
|
handle_select([...index_path, i], [...path, name], type);
|
|
}
|
|
}}
|
|
onkeydown={(e) => {
|
|
if (selectable && (e.key === " " || e.key === "Enter")) {
|
|
handle_select([...index_path, i], [...path, name], type);
|
|
}
|
|
}}
|
|
>
|
|
{#if interactive}
|
|
{#if type === "folder" && file_count === "single"}
|
|
<span class="no-checkbox" aria-hidden="true"></span>
|
|
{:else}
|
|
<Checkbox
|
|
disabled={false}
|
|
value={is_selected}
|
|
onchange={(checked) => {
|
|
oncheck?.({
|
|
path: [...path, name],
|
|
checked,
|
|
type
|
|
});
|
|
if (selectable) {
|
|
handle_select([...index_path, i], [...path, name], type);
|
|
}
|
|
if (type === "folder" && checked) {
|
|
open_folder(i);
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if type === "folder"}
|
|
<span
|
|
class="icon"
|
|
class:hidden={!opened_folders.includes(i)}
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
toggle_open_folder(i);
|
|
}}
|
|
role="button"
|
|
aria-label="expand directory"
|
|
tabindex="0"
|
|
onkeydown={(e) => {
|
|
if (e.key === " " || e.key === "Enter") {
|
|
toggle_open_folder(i);
|
|
}
|
|
}}><Arrow /></span
|
|
>
|
|
{:else}
|
|
<span class="file-icon">
|
|
<img src={name === "." ? FolderIcon : FileIcon} alt="file icon" />
|
|
</span>
|
|
{/if}
|
|
<span class="item-name">{name}</span>
|
|
</span>
|
|
{#if type === "folder" && opened_folders.includes(i)}
|
|
<Self
|
|
path={[...path, name]}
|
|
index_path={[...index_path, i]}
|
|
selected_files={selected_files
|
|
.filter((x) => x[0] === name)
|
|
.map((x) => x.slice(1))}
|
|
selected_folders={selected_folders
|
|
.filter((x) => x[0] === name)
|
|
.map((x) => x.slice(1))}
|
|
is_selected_entirely={selected_folders.some(
|
|
(x) => x[0] === name && x.length === 1
|
|
)}
|
|
{interactive}
|
|
{selectable}
|
|
{ls_fn}
|
|
{file_count}
|
|
valid_for_selection={valid ?? false}
|
|
{oncheck}
|
|
{onselect}
|
|
/>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<style>
|
|
.icon {
|
|
display: inline-block;
|
|
width: 18px;
|
|
height: 18px;
|
|
padding: 3px 2px 3px 3px;
|
|
margin: 0;
|
|
flex-grow: 0;
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
transition: 0.1s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.file-icon {
|
|
display: inline-block;
|
|
height: 20px;
|
|
margin-left: -1px;
|
|
margin: 0;
|
|
flex-grow: 0;
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
transition: 0.1s;
|
|
}
|
|
|
|
.file-icon img {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.icon:hover {
|
|
background: #eee;
|
|
}
|
|
|
|
.icon:hover :global(> *) {
|
|
color: var(--block-info-text-color);
|
|
}
|
|
|
|
.icon :global(> *) {
|
|
transform: rotate(90deg);
|
|
transform-origin: 40% 50%;
|
|
transition: 0.2s;
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
.no-checkbox {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
ul.root .no-checkbox {
|
|
display: none;
|
|
}
|
|
|
|
.hidden :global(> *) {
|
|
transform: rotate(0);
|
|
color: var(--body-text-color-subdued);
|
|
}
|
|
|
|
ul {
|
|
margin-left: 26px;
|
|
padding-left: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
ul.root {
|
|
margin-left: 8px;
|
|
}
|
|
|
|
ul.no-checkboxes:not(.root) {
|
|
margin-left: 20px;
|
|
}
|
|
|
|
li {
|
|
margin-left: 0;
|
|
padding-left: 0;
|
|
align-items: center;
|
|
margin: 8px 0;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--scale-00);
|
|
overflow-wrap: anywhere;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.wrap {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.wrap.selected {
|
|
background-color: var(--color-accent-soft);
|
|
border-radius: var(--radius-sm);
|
|
margin-left: -4px;
|
|
padding-left: 4px;
|
|
}
|
|
|
|
.wrap.selectable {
|
|
cursor: pointer;
|
|
border-radius: var(--radius-sm);
|
|
margin-left: -4px;
|
|
padding-left: 4px;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
.wrap.selectable:hover {
|
|
background-color: var(--border-color-accent);
|
|
}
|
|
|
|
.item-name {
|
|
flex: 1;
|
|
padding: 2px 4px;
|
|
border-radius: var(--radius-sm);
|
|
margin-right: -4px;
|
|
}
|
|
</style>
|