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
416 lines
10 KiB
Svelte
416 lines
10 KiB
Svelte
<script lang="ts">
|
|
import { _ } from "svelte-i18n";
|
|
import { BlockTitle, IconButtonWrapper } from "@gradio/atoms";
|
|
import { Remove, DropdownArrow } from "@gradio/icons";
|
|
import type { Gradio } from "@gradio/utils";
|
|
import DropdownOptions from "./DropdownOptions.svelte";
|
|
import { handle_filter, handle_shared_keys } from "./utils";
|
|
import type { DropdownEvents, DropdownProps, Item } from "../types.ts";
|
|
|
|
const props = $props();
|
|
|
|
const gradio: Gradio<DropdownEvents, DropdownProps> = props.gradio;
|
|
|
|
let filter_input: HTMLElement;
|
|
let input_text = $state("");
|
|
let label = $derived(gradio.shared.label || "Multiselect");
|
|
|
|
const uid = $props.id();
|
|
const listbox_id = `${uid}-options`;
|
|
let buttons = $derived(gradio.props.buttons);
|
|
|
|
// Translate the display side only; values stay raw for event payloads.
|
|
let translated_choices: [string, string | number][] = $derived.by(() => {
|
|
return gradio.props.choices.map(
|
|
([display, value]) =>
|
|
[gradio.live_i18n(display), value] as [string, string | number]
|
|
);
|
|
});
|
|
let choices_names: string[] = $derived.by(() => {
|
|
return translated_choices.map((c) => c[0]);
|
|
});
|
|
let choices_values: (string | number)[] = $derived.by(() => {
|
|
return gradio.props.choices.map((c) => c[1]);
|
|
});
|
|
|
|
let disabled = $derived(!gradio.shared.interactive);
|
|
|
|
let show_options = $state(false);
|
|
|
|
// All of these are indices with respect to the choices array
|
|
let [filtered_indices, active_index] = $derived.by(() => {
|
|
const filtered = handle_filter(translated_choices, input_text);
|
|
return [
|
|
filtered,
|
|
filtered.length > 0 && !gradio.props.allow_custom_value
|
|
? filtered[0]
|
|
: null
|
|
];
|
|
});
|
|
|
|
function set_selected_indices(): Item[] {
|
|
if (gradio.props.value === undefined) {
|
|
return [];
|
|
} else if (Array.isArray(gradio.props.value)) {
|
|
return gradio.props.value
|
|
.map((v) => {
|
|
const index = choices_values.indexOf(v);
|
|
if (index !== -1) {
|
|
return index;
|
|
}
|
|
if (gradio.props.allow_custom_value) {
|
|
return v;
|
|
}
|
|
// Instead of returning null, skip this iteration
|
|
return undefined;
|
|
})
|
|
.filter((val): val is string | number => val !== undefined);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
let selected_indices: (number | string)[] = $derived.by(set_selected_indices);
|
|
|
|
function handle_blur(): void {
|
|
if (!gradio.props.allow_custom_value) {
|
|
input_text = "";
|
|
}
|
|
|
|
if (gradio.props.allow_custom_value && input_text !== "") {
|
|
add_selected_choice(input_text);
|
|
input_text = "";
|
|
}
|
|
gradio.dispatch("blur");
|
|
show_options = false;
|
|
active_index = null;
|
|
}
|
|
|
|
function remove_selected_choice(option_index: number | string) {
|
|
selected_indices = selected_indices.filter((v) => v !== option_index);
|
|
gradio.props.value = selected_indices.map((index) =>
|
|
typeof index === "number" ? choices_values[index] : index
|
|
);
|
|
gradio.dispatch("input");
|
|
gradio.dispatch("select", {
|
|
index: typeof option_index === "number" ? option_index : -1,
|
|
value:
|
|
typeof option_index === "number"
|
|
? choices_values[option_index]
|
|
: option_index,
|
|
selected: false
|
|
});
|
|
}
|
|
|
|
function add_selected_choice(option_index: number | string) {
|
|
if (
|
|
gradio.props.max_choices == null ||
|
|
selected_indices.length < gradio.props.max_choices
|
|
) {
|
|
selected_indices.push(option_index);
|
|
gradio.dispatch("select", {
|
|
index: typeof option_index === "number" ? option_index : -1,
|
|
value:
|
|
typeof option_index === "number"
|
|
? choices_values[option_index]
|
|
: option_index,
|
|
selected: true
|
|
});
|
|
}
|
|
if (selected_indices.length === gradio.props.max_choices) {
|
|
show_options = false;
|
|
active_index = null;
|
|
filter_input.blur();
|
|
}
|
|
gradio.props.value = selected_indices.map((index) =>
|
|
typeof index === "number" ? choices_values[index] : index
|
|
);
|
|
}
|
|
|
|
function handle_option_selected(index: any): void {
|
|
const option_index = parseInt(index);
|
|
add_or_remove_index(option_index);
|
|
}
|
|
|
|
function add_or_remove_index(option_index: number): void {
|
|
if (selected_indices.includes(option_index)) {
|
|
remove_selected_choice(option_index);
|
|
} else {
|
|
add_selected_choice(option_index);
|
|
}
|
|
input_text = "";
|
|
active_index = null;
|
|
gradio.dispatch("input");
|
|
}
|
|
|
|
function remove_all(e: any): void {
|
|
selected_indices = [];
|
|
input_text = "";
|
|
gradio.props.value = [];
|
|
e.preventDefault();
|
|
}
|
|
|
|
function handle_focus(e: FocusEvent): void {
|
|
filtered_indices = gradio.props.choices.map((_, i) => i);
|
|
if (
|
|
gradio.props.max_choices === null ||
|
|
selected_indices.length < gradio.props.max_choices
|
|
) {
|
|
show_options = true;
|
|
}
|
|
gradio.dispatch("focus");
|
|
show_options = true;
|
|
}
|
|
|
|
function handle_key_down(e: KeyboardEvent): void {
|
|
[show_options, active_index] = handle_shared_keys(
|
|
e,
|
|
active_index,
|
|
filtered_indices
|
|
);
|
|
if (e.key === "Enter") {
|
|
if (active_index !== null) {
|
|
add_or_remove_index(active_index);
|
|
} else {
|
|
if (gradio.props.allow_custom_value) {
|
|
add_selected_choice(input_text);
|
|
input_text = "";
|
|
}
|
|
}
|
|
}
|
|
if (e.key === "Backspace" && input_text === "") {
|
|
const new_indices = selected_indices.slice(0, -1);
|
|
gradio.props.value = new_indices.map((index) =>
|
|
typeof index === "number" ? choices_values[index] : index
|
|
);
|
|
}
|
|
if (selected_indices.length === gradio.props.max_choices) {
|
|
show_options = false;
|
|
active_index = null;
|
|
}
|
|
}
|
|
|
|
let old_value = $state(gradio.props.value);
|
|
|
|
$effect(() => {
|
|
if (old_value !== gradio.props.value) {
|
|
old_value = gradio.props.value;
|
|
gradio.dispatch("change");
|
|
}
|
|
});
|
|
|
|
function oncustom_button_click(id: number) {
|
|
gradio.dispatch("custom_button_click", { id });
|
|
}
|
|
</script>
|
|
|
|
<div class:container={gradio.shared.container}>
|
|
{#if gradio.shared.show_label && buttons && buttons.length > 0}
|
|
<IconButtonWrapper
|
|
{buttons}
|
|
on_custom_button_click={oncustom_button_click}
|
|
/>
|
|
{/if}
|
|
<BlockTitle show_label={gradio.shared.show_label} info={gradio.props.info}
|
|
>{label}</BlockTitle
|
|
>
|
|
|
|
<div class="wrap">
|
|
<div class="wrap-inner" class:show_options>
|
|
{#each selected_indices as s}
|
|
<div class="token">
|
|
<span>
|
|
{#if typeof s === "number"}
|
|
{choices_names[s]}
|
|
{:else}
|
|
{s}
|
|
{/if}
|
|
</span>
|
|
{#if !disabled}
|
|
<div
|
|
class="token-remove"
|
|
on:click|preventDefault={() => remove_selected_choice(s)}
|
|
on:keydown={(event) => {
|
|
if (event.key === "Enter") {
|
|
remove_selected_choice(s);
|
|
}
|
|
}}
|
|
role="button"
|
|
tabindex="0"
|
|
title={gradio.i18n("common.remove") + " " + s}
|
|
>
|
|
<Remove />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
<div class="secondary-wrap">
|
|
<input
|
|
role="combobox"
|
|
aria-controls={listbox_id}
|
|
aria-expanded={show_options}
|
|
aria-activedescendant={show_options && active_index !== null
|
|
? `${listbox_id}-option-${active_index}`
|
|
: undefined}
|
|
aria-autocomplete={gradio.props.filterable ? "list" : "none"}
|
|
aria-label={label}
|
|
class="border-none"
|
|
class:subdued={(!choices_names.includes(input_text) &&
|
|
!gradio.props.allow_custom_value) ||
|
|
selected_indices.length === gradio.props.max_choices}
|
|
{disabled}
|
|
autocomplete="off"
|
|
bind:value={input_text}
|
|
bind:this={filter_input}
|
|
on:keydown={handle_key_down}
|
|
on:keyup={(e) => {
|
|
gradio.dispatch("key_up", {
|
|
key: e.key,
|
|
input_value: input_text
|
|
});
|
|
}}
|
|
on:blur={handle_blur}
|
|
on:focus={handle_focus}
|
|
readonly={!gradio.props.filterable}
|
|
/>
|
|
|
|
{#if !disabled}
|
|
{#if selected_indices.length > 0}
|
|
<div
|
|
role="button"
|
|
tabindex="0"
|
|
class="token-remove remove-all"
|
|
title={gradio.i18n("common.clear")}
|
|
on:click={remove_all}
|
|
on:keydown={(event) => {
|
|
if (event.key === "Enter") {
|
|
remove_all(event);
|
|
}
|
|
}}
|
|
>
|
|
<Remove />
|
|
</div>
|
|
{/if}
|
|
<span class="icon-wrap"> <DropdownArrow /></span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<DropdownOptions
|
|
{show_options}
|
|
choices={translated_choices}
|
|
{filtered_indices}
|
|
{disabled}
|
|
{selected_indices}
|
|
{active_index}
|
|
{listbox_id}
|
|
remember_scroll={true}
|
|
onchange={handle_option_selected}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.icon-wrap {
|
|
color: var(--body-text-color);
|
|
margin-right: var(--size-2);
|
|
width: var(--size-5);
|
|
}
|
|
.container .wrap {
|
|
box-shadow: var(--input-shadow);
|
|
border: var(--input-border-width) solid var(--border-color-primary);
|
|
}
|
|
|
|
.wrap {
|
|
position: relative;
|
|
border-radius: var(--input-radius);
|
|
background: var(--input-background-fill);
|
|
}
|
|
|
|
.wrap:focus-within {
|
|
box-shadow: var(--input-shadow-focus);
|
|
border-color: var(--input-border-color-focus);
|
|
}
|
|
|
|
.wrap-inner {
|
|
display: flex;
|
|
position: relative;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: var(--checkbox-label-gap);
|
|
padding: var(--checkbox-label-padding);
|
|
}
|
|
|
|
.token {
|
|
display: flex;
|
|
align-items: center;
|
|
transition: var(--button-transition);
|
|
cursor: pointer;
|
|
box-shadow: var(--checkbox-label-shadow);
|
|
border: var(--checkbox-label-border-width) solid
|
|
var(--checkbox-label-border-color);
|
|
border-radius: var(--button-small-radius);
|
|
background: var(--checkbox-label-background-fill);
|
|
padding: var(--checkbox-label-padding);
|
|
color: var(--checkbox-label-text-color);
|
|
font-weight: var(--checkbox-label-text-weight);
|
|
font-size: var(--checkbox-label-text-size);
|
|
line-height: var(--line-md);
|
|
word-break: break-word;
|
|
}
|
|
|
|
.token > * + * {
|
|
margin-left: var(--size-2);
|
|
}
|
|
|
|
.token-remove {
|
|
fill: var(--body-text-color);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
border: var(--checkbox-border-width) solid var(--border-color-primary);
|
|
border-radius: var(--radius-full);
|
|
background: var(--background-fill-primary);
|
|
padding: var(--size-0-5);
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.secondary-wrap {
|
|
display: flex;
|
|
flex: 1 1 0%;
|
|
align-items: center;
|
|
border: none;
|
|
min-width: min-content;
|
|
}
|
|
|
|
input {
|
|
margin: var(--spacing-sm);
|
|
outline: none;
|
|
border: none;
|
|
background: inherit;
|
|
width: var(--size-full);
|
|
color: var(--body-text-color);
|
|
font-size: var(--input-text-size);
|
|
}
|
|
|
|
input:disabled {
|
|
-webkit-text-fill-color: var(--body-text-color);
|
|
-webkit-opacity: 1;
|
|
opacity: 1;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.remove-all {
|
|
margin-left: var(--size-1);
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
.subdued {
|
|
color: var(--body-text-color-subdued);
|
|
}
|
|
input[readonly] {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|