Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

329 lines
7.6 KiB
Svelte
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import type { FileData } from "@gradio/client";
import { prettyBytes } from "./utils";
import type { I18nFormatter, SelectData } from "@gradio/utils";
import { DownloadLink } from "@gradio/atoms";
let {
value,
selectable = false,
height = undefined,
i18n,
allow_reordering = false,
onselect,
onchange,
ondelete,
ondownload
}: {
value: FileData | FileData[];
selectable?: boolean;
height?: number | string | undefined;
i18n: I18nFormatter;
allow_reordering?: boolean;
onselect?: (event_data: SelectData) => void;
onchange?: (event_data: FileData[] | FileData) => void;
ondelete?: (event_data: FileData) => void;
ondownload?: (event_data: FileData) => void;
} = $props();
let dragging_index: number | null = $state(null);
let drop_target_index: number | null = $state(null);
function handle_drag_start(event: DragEvent, index: number): void {
dragging_index = index;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", index.toString());
}
}
function handle_drag_over(event: DragEvent, index: number): void {
event.preventDefault();
if (index === normalized_files.length - 1) {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
const midY = rect.top + rect.height / 2;
drop_target_index =
event.clientY > midY ? normalized_files.length : index;
} else {
drop_target_index = index;
}
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
}
function handle_drag_end(event: DragEvent): void {
if (
!event.dataTransfer?.dropEffect ||
event.dataTransfer.dropEffect === "none"
) {
dragging_index = null;
drop_target_index = null;
}
}
function handle_drop(event: DragEvent, index: number): void {
event.preventDefault();
if (dragging_index === null || dragging_index === index) return;
const files = Array.isArray(value) ? [...value] : [value];
const [removed] = files.splice(dragging_index, 1);
files.splice(
drop_target_index === normalized_files.length
? normalized_files.length
: index,
0,
removed
);
const new_value = Array.isArray(value) ? files : files[0];
onchange?.(new_value);
dragging_index = null;
drop_target_index = null;
}
function split_filename(filename: string): [string, string] {
const last_dot = filename.lastIndexOf(".");
if (last_dot === -1) {
return [filename, ""];
}
return [filename.slice(0, last_dot), filename.slice(last_dot)];
}
let normalized_files = $derived(
(Array.isArray(value) ? value : [value]).map((file) => {
const [filename_stem, filename_ext] = split_filename(
file.orig_name ?? ""
);
return {
...file,
filename_stem,
filename_ext
};
})
);
function handle_row_click(
event: MouseEvent & { currentTarget: HTMLTableRowElement },
index: number
): void {
const tr = event.currentTarget;
const should_select =
event.target === tr || // Only select if the click is on the row itself
(tr &&
tr.firstElementChild &&
event.composedPath().includes(tr.firstElementChild)); // Or if the click is on the name column
if (should_select) {
onselect?.({ value: normalized_files[index].orig_name, index });
}
}
function remove_file(index: number): void {
const files = Array.isArray(value) ? [...value] : [value];
const removed = files.splice(index, 1);
const new_value = Array.isArray(value) ? files : files[0];
value = new_value;
ondelete?.(removed[0]);
onchange?.(new_value);
}
function handle_download(file: FileData): void {
ondownload?.(file);
}
const is_browser = typeof window !== "undefined";
</script>
<div
class="file-preview-holder"
style:max-height={height
? typeof height === "number"
? height + "px"
: height
: "auto"}
>
<table class="file-preview">
<tbody>
{#each normalized_files as file, i (file.url)}
<tr
class="file"
class:selectable
class:dragging={dragging_index === i}
class:drop-target={drop_target_index === i ||
(i === normalized_files.length - 1 &&
drop_target_index === normalized_files.length)}
data-drop-target={drop_target_index === normalized_files.length &&
i === normalized_files.length - 1
? "after"
: drop_target_index === i + 1
? "after"
: "before"}
draggable={allow_reordering && normalized_files.length > 1}
onclick={(event) => {
handle_row_click(event, i);
}}
ondragstart={(event) => handle_drag_start(event, i)}
ondragenter={(event) => event.preventDefault()}
ondragover={(event) => handle_drag_over(event, i)}
ondrop={(event) => handle_drop(event, i)}
ondragend={handle_drag_end}
>
<td class="filename" aria-label={file.orig_name}>
{#if allow_reordering && normalized_files.length > 1}
<span class="drag-handle">⋮⋮</span>
{/if}
<span class="stem">{file.filename_stem}</span>
<span class="ext">{file.filename_ext}</span>
</td>
<td class="download">
{#if file.url}
<DownloadLink
href={file.url}
on:click={() => handle_download(file)}
download={is_browser && window.__is_colab__
? null
: file.orig_name}
>
{@html file.size != null
? prettyBytes(file.size)
: "(size unknown)"}&nbsp;&#8675;
</DownloadLink>
{:else}
{i18n("file.uploading")}
{/if}
</td>
{#if normalized_files.length > 1}
<td>
<button
class="label-clear-button"
aria-label="Remove this file"
onclick={() => {
remove_file(i);
}}
onkeydown={(event) => {
if (event.key === "Enter") {
remove_file(i);
}
}}
>×
</button>
</td>
{/if}
</tr>
{/each}
</tbody>
</table>
</div>
<style>
.label-clear-button {
color: var(--body-text-color-subdued);
position: relative;
left: -3px;
}
.label-clear-button:hover {
color: var(--body-text-color);
}
.file-preview {
table-layout: fixed;
width: var(--size-full);
max-height: var(--size-60);
overflow-y: auto;
margin-top: var(--size-1);
color: var(--body-text-color);
}
.file-preview-holder {
overflow: auto;
}
.file {
display: flex;
width: var(--size-full);
}
.file > * {
padding: var(--size-1) var(--size-2-5);
}
.filename {
flex-grow: 1;
display: flex;
overflow: hidden;
}
.filename .stem {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.filename .ext {
white-space: nowrap;
}
.download {
min-width: 8rem;
width: 10%;
white-space: nowrap;
text-align: right;
}
.download:hover {
text-decoration: underline;
}
.download > :global(a) {
color: var(--link-text-color);
}
.download > :global(a:hover) {
color: var(--link-text-color-hover);
}
.download > :global(a:visited) {
color: var(--link-text-color-visited);
}
.download > :global(a:active) {
color: var(--link-text-color-active);
}
.selectable {
cursor: pointer;
}
tbody > tr:nth-child(even) {
background: var(--block-background-fill);
}
tbody > tr:nth-child(odd) {
background: var(--table-odd-background-fill);
}
.drag-handle {
cursor: grab;
color: var(--body-text-color-subdued);
padding-right: var(--size-2);
user-select: none;
}
.dragging {
opacity: 0.5;
cursor: grabbing;
}
.drop-target {
border-top: 2px solid var(--color-accent);
}
tr:last-child.drop-target[data-drop-target="before"] {
border-top: 2px solid var(--color-accent);
border-bottom: none;
}
tr:last-child.drop-target[data-drop-target="after"] {
border-top: none;
border-bottom: 2px solid var(--color-accent);
}
</style>