Use melt combobox over native select

This commit is contained in:
WardPearce
2026-03-13 11:45:45 +13:00
parent 8e75a067da
commit 8140970edb
6 changed files with 188 additions and 252 deletions
@@ -7,20 +7,20 @@
let {
options,
label,
label = undefined,
defaultValue = undefined,
onChange = undefined
}: {
options: ComboOption[];
label: string;
defaultValue?: string;
label?: string;
defaultValue?: string | null;
onChange?: (value: string) => void;
} = $props();
let initialValueChange = true;
const combobox = new Combobox<string>({
onValueChange: (value) => {
if (initialValueChange && typeof defaultValue !== 'undefined') {
if (initialValueChange && defaultValue) {
initialValueChange = false;
return;
}
@@ -53,13 +53,15 @@
});
</script>
<div class="field label suffix surface-container-highest">
<div class="field suffix surface-container-highest" class:label={typeof label === 'string'}>
<input
{...mergeAttrs(combobox.input, {
value: valueToLabel[combobox.input.value]
})}
/>
<label {...combobox.label}>{label}</label>
{#if label}
<label {...combobox.label}>{label}</label>
{/if}
<i>keyboard_arrow_down</i>
<div {...combobox.content}>
@@ -12,6 +12,7 @@
import { addToast } from '../Toast.svelte';
import { Clipboard } from '@capacitor/clipboard';
import { downloadStringAsFile } from '$lib/misc';
import ComboBox from '../ComboBox.svelte';
let remoteFilterListUrl: string = $state($filterContentUrlStore ?? '');
let remoteError: string = $state('');
@@ -165,17 +166,21 @@
<article class="no-margin surface-container-high">
<div class="grid">
<div class="s12 m6 l6">
<div class="label field suffix surface-container-highest">
<select name="content-type">
{#each filterTypes as filterType (filterType)}
<option selected={filterType === filter.type} value={filterType}
>{titleCase(filterType)}</option
>
{/each}
</select>
<label for="content-type">{$_('layout.filter.contentType')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('layout.filter.contentType')}
defaultValue={filter.type}
options={filterTypes.map((filterType) => {
return {
label: titleCase(filterType),
value: filterType
};
})}
onChange={(value) => {
filter.type = value as 'channel' | 'video';
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
/>
</div>
<div class="s12 m6 l6 right-align">
<button onclick={() => removeFilter(filter)} class="surface-container-highest">
@@ -203,89 +208,70 @@
</button>
</nav>
<div class="label field suffix surface-container-highest">
<select
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
condition.field = event.currentTarget.value;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
name="field"
>
{#each Object.keys(schema[filter.type]) as key (key)}
<option value={key} selected={condition.field === key}
>{camelCaseToHuman(key)}</option
>
{/each}
</select>
<label for="field">{$_('layout.filter.field')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('layout.filter.field')}
defaultValue={condition.field}
options={Object.keys(schema[filter.type]).map((key) => {
return {
label: camelCaseToHuman(key),
value: key
};
})}
onChange={(value) => {
condition.field = value;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
/>
<div class="field label suffix surface-container-highest">
<select
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
condition.operator = event.currentTarget.value as z.infer<
typeof zFilterOperatorEnum
>;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
name="operator"
>
{#each zFilterOperatorEnum.options as operator (operator)}
<option selected={operator === condition.operator} value={operator}
>{operator}</option
>
{/each}
</select>
<label for="operator">{$_('layout.filter.operator')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('layout.filter.operator')}
defaultValue={condition.operator}
options={zFilterOperatorEnum.options.map((operator) => {
return {
label: operator,
value: operator
};
})}
onChange={(value) => {
condition.operator = value as z.infer<typeof zFilterOperatorEnum>;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
/>
{#each condition.values as conditionValue, index (index)}
<nav>
{#if schema[filter.type][condition.field] === 'boolean'}
<div class="field label suffix surface-container-highest max">
<select
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
condition.values[index] = event.currentTarget.value;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
name="boolean-options"
>
<option value="" disabled selected
>{$_('layout.filter.optionPlaceholder')}</option
>
<option selected={conditionValue === 'true'} value="true">true</option>
<option selected={conditionValue === 'false'} value="false">false</option>
</select>
<label for="boolean-options">Value</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label="Value"
defaultValue={conditionValue as string}
options={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' }
]}
onChange={(value) => {
condition.values[index] = value;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
/>
{:else if Array.isArray(schema[filter.type][condition.field])}
<div class="field label suffix surface-container-highest max">
<select
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
condition.values[index] = event.currentTarget.value;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
name="array-options"
>
<option value="" disabled selected
>{$_('layout.filter.optionPlaceholder')}</option
>
{#each schema[filter.type][condition.field] as value (value)}
<option selected={conditionValue === value} {value}
>{camelCaseToHuman(value)}</option
>
{/each}
</select>
<label for="array-options">Value</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label="Value"
defaultValue={conditionValue as string}
options={(schema[filter.type][condition.field] as string[]).map((value) => {
return {
label: camelCaseToHuman(value),
value
};
})}
onChange={(value) => {
condition.values[index] = value;
filterContentListStore.set(contentFilters);
filterContentUrlAutoUpdateStore.set(false);
}}
/>
{:else}
<div class="field label surface-container-highest max">
<input
@@ -1,6 +1,6 @@
<script lang="ts">
import { bookmarkletSaveToUrl } from '$lib/externalSettings/index';
import { letterCase, titleCases } from '$lib/letterCasing';
import { letterCase, titleCases, type TitleCase } from '$lib/letterCasing';
import { setAmoledTheme } from '$lib/theme';
import { Capacitor } from '@capacitor/core';
import ui from 'beercss';
@@ -38,9 +38,6 @@
import ComboBox from '../ComboBox.svelte';
let invidiousInstance = $state(get(invidiousInstanceStore));
let region = $state(get(interfaceRegionStore));
let forceCase = $state(get(interfaceForceCase));
let defaultPage = $state(get(interfaceDefaultPage));
let invalidInstance = $state(false);
let colorPickerOpen = $state(false);
@@ -396,58 +393,41 @@
</div>
{/if}
<div class="field label suffix surface-container-highest">
<select
tabindex="0"
name="region"
bind:value={region}
onchange={() => interfaceRegionStore.set(region)}
>
{#each iso31661 as region (region)}
<option selected={$interfaceRegionStore === region.alpha2} value={region.alpha2}
>{region.alpha2} - {region.name}</option
>
{/each}
</select>
<label tabindex="-1" for="region">{$_('region')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('region')}
defaultValue={$interfaceRegionStore}
options={iso31661.map((region) => {
return {
label: region.name,
value: region.alpha2
};
})}
onChange={(value) => interfaceRegionStore.set(value)}
/>
<div class="field label suffix surface-container-highest">
<select
tabindex="0"
name="case"
bind:value={forceCase}
onchange={() => interfaceForceCase.set(forceCase)}
>
<option selected={$interfaceForceCase === null} value={null}>Default</option>
{#each titleCases as caseType (caseType)}
<option selected={$interfaceForceCase === caseType} value={caseType}
>{letterCase(`${caseType}`, caseType)}</option
>
{/each}
</select>
<label tabindex="-1" for="case">{$_('letterCase')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('letterCase')}
defaultValue={$interfaceForceCase}
options={titleCases.map((caseType) => {
return {
label: letterCase(`${caseType}`, caseType),
value: caseType as string
};
})}
onChange={(value) => interfaceForceCase.set(value as TitleCase)}
/>
<div class="field label suffix surface-container-highest">
<select
name="defaultPage"
tabindex="0"
bind:value={defaultPage}
onchange={() => interfaceDefaultPage.set(defaultPage)}
>
{#each pages as page (page)}
{#if !page.requiresAuth || get(invidiousAuthStore)}
<option selected={$interfaceDefaultPage === page.href} value={page.href}>{page.name}</option
>
{/if}
{/each}
</select>
<label tabindex="-1" for="defaultPage">{$_('defaultPage')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('defaultPage')}
defaultValue={$interfaceDefaultPage}
options={pages.map((page) => {
return {
label: page.name,
value: page.href
};
})}
onChange={(value) => interfaceDefaultPage.set(value)}
/>
{#if !Capacitor.isNativePlatform()}
<div class="space"></div>
@@ -24,8 +24,7 @@
} from '../../store';
import { playbackRates } from '$lib/player/index';
import { isUnrestrictedPlatform } from '$lib/misc';
let defaultLanguage = $state(get(playerDefaultLanguage));
import ComboBox from '../ComboBox.svelte';
let localVideoFallback: 'enabled' | 'disabled' | 'always' = $state('enabled');
if (!$playerYouTubeJsFallback) {
@@ -38,18 +37,10 @@
ISO6391.getName(code).toLocaleLowerCase()
);
function onQualityChange(event: Event) {
const value = (event.target as HTMLSelectElement).value;
playerDefaultQualityStore.set(value);
}
languageNames.unshift('original');
function onSpeedChange(event: Event) {
const value = (event.target as HTMLSelectElement).value;
playerDefaultPlaybackSpeed.set(Number(value));
}
function onLocalVideoFallbackChange() {
switch (localVideoFallback) {
function onLocalVideoFallbackChange(value: string) {
switch (value) {
case 'enabled':
playerYouTubeJsFallback.set(true);
playerYouTubeJsAlways.set(false);
@@ -67,79 +58,62 @@
</script>
<div class="margin"></div>
<div class="field label suffix surface-container-highest">
<select
tabindex="0"
name="case"
bind:value={defaultLanguage}
onchange={() => playerDefaultLanguage.set(defaultLanguage)}
>
<option selected={$playerDefaultLanguage === 'original'} value="original">Original</option>
{#each languageNames as language (language)}
<option selected={$playerDefaultLanguage === language} value={language}
>{titleCase(language)}</option
>
{/each}
</select>
<label tabindex="-1" for="case">{$_('player.defaultLanguage')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('player.defaultLanguage')}
defaultValue={$playerDefaultLanguage}
options={languageNames.map((language) => {
return {
label: titleCase(language),
value: language
};
})}
onChange={(value) => playerDefaultLanguage.set(value)}
/>
<div class="field label suffix surface-container-highest">
<select
tabindex="0"
name="quality"
id="quality"
bind:value={$playerDefaultQualityStore}
onchange={onQualityChange}
>
<option value="auto">Auto (Recommended)</option>
<option value="144">144p (Ultra low)</option>
<option value="240">240p (Low)</option>
<option value="360">360p (SD)</option>
<option value="480">480p (SD+)</option>
<option value="720">720p (HD)</option>
<option value="1080">1080p (Full HD)</option>
<option value="1440">1440p (2K)</option>
<option value="2160">2160p (4K UHD)</option>
</select>
<label tabindex="-1" for="quality">{$_('player.preferredQuality')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('player.preferredQuality')}
defaultValue={$playerDefaultQualityStore}
onChange={(value) => playerDefaultQualityStore.set(value)}
options={[
{ label: 'Auto (Recommended)', value: 'auto' },
{ label: '144p (Ultra low)', value: '144' },
{ label: '240p (Low)', value: '240' },
{ label: '360p (SD)', value: '360' },
{ label: '480p (SD+)', value: '480' },
{ label: '720p (HD)', value: '720' },
{ label: '1080p (Full HD)', value: '1080' },
{ label: '1440p (2K)', value: '1440' },
{ label: '2160p (4K UHD)', value: '2160' }
]}
/>
<div class="field label suffix surface-container-highest">
<select
tabindex="0"
name="quality"
id="quality"
bind:value={$playerDefaultPlaybackSpeed}
onchange={onSpeedChange}
>
{#each playbackRates as speed (speed)}
<option value={speed}>x{speed}</option>
{/each}
</select>
<label tabindex="-1" for="quality">{$_('layout.player.defaultPlaybackSpeed')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('layout.player.defaultPlaybackSpeed')}
defaultValue={$playerDefaultPlaybackSpeed.toString()}
options={playbackRates.map((rate) => {
return {
label: `x${rate}`,
value: rate.toString()
};
})}
onChange={(value) => playerDefaultPlaybackSpeed.set(Number(value))}
/>
{#if isUnrestrictedPlatform() && $backendInUseStore === 'ivg'}
<div class="field suffix surface-container-highest label">
<select
tabindex="0"
name="ytfallback"
bind:value={localVideoFallback}
onchange={onLocalVideoFallbackChange}
>
<option value="enabled">{$_('enabled')}</option>
<option value="always">{$_('layout.player.youtubeJsAlways')}</option>
<option value="disabled">{$_('disabled')}</option>
</select>
<label tabindex="-1" for="ytfallback">{$_('layout.player.localVideoFallback')}</label>
<i>arrow_drop_down</i>
</div>
<ComboBox
label={$_('layout.player.localVideoFallback')}
defaultValue={localVideoFallback}
options={[
{ label: $_('enabled'), value: 'enabled' },
{ label: $_('layout.player.youtubeJsAlways'), value: 'always' },
{ label: $_('disabled'), value: 'disabled' }
]}
onChange={(value) => onLocalVideoFallbackChange(value)}
/>
{/if}
<div class="space"></div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
@@ -9,6 +9,7 @@
sponsorBlockTimelineStore,
sponsorBlockUrlStore
} from '../../store';
import ComboBox from '../ComboBox.svelte';
let sponsorBlockInstance = $state(get(sponsorBlockUrlStore));
@@ -22,11 +23,8 @@
{ name: $_('layout.sponsors.tangentJokes'), category: 'filler' }
];
function onSponsorSet(
category: string,
event: Event & { currentTarget: EventTarget & HTMLSelectElement }
) {
const value = event.currentTarget.value as 'automatic' | 'manual' | 'timeline' | 'disabled';
function onSponsorSet(category: string, givenValue: string) {
const value = givenValue as 'automatic' | 'manual' | 'timeline' | 'disabled';
const categories = get(sponsorBlockCategoriesStore);
@@ -112,29 +110,23 @@
<p class="bold">{$_('layout.sponsors.Catagories')}</p>
{#each sponsorCategories as sponsor (sponsor)}
{@const currentCatergoryTrigger = $sponsorBlockCategoriesStore[sponsor.category]}
<div class="field middle-align no-margin">
{@const currentCategoryTrigger = $sponsorBlockCategoriesStore[sponsor.category]}
<div class="field middle-align">
<nav class="no-padding">
<div class="max">
<p>{sponsor.name}</p>
</div>
<div class="field suffix surface-container-highest">
<select onchange={(event) => onSponsorSet(sponsor.category, event)}>
<option selected={currentCatergoryTrigger === undefined} value="disabled"
>{$_('disabled')}</option
>
<option selected={currentCatergoryTrigger === 'automatic'} value="automatic"
>{$_('layout.sponsors.automatic')}</option
>
<option selected={currentCatergoryTrigger === 'manual'} value="manual"
>{$_('layout.sponsors.manual')}</option
>
<option selected={currentCatergoryTrigger === 'timeline'} value="timeline"
>{$_('layout.sponsors.timeline')}</option
>
</select>
<i>arrow_drop_down</i>
</div>
<ComboBox
options={[
{ label: $_('disabled'), value: 'disabled' },
{ label: $_('layout.sponsors.automatic'), value: 'automatic' },
{ label: $_('layout.sponsors.manual'), value: 'manual' },
{ label: $_('layout.sponsors.timeline'), value: 'timeline' }
]}
defaultValue={currentCategoryTrigger ?? 'disabled'}
onChange={(value) => onSponsorSet(sponsor.category, value)}
/>
</nav>
</div>
{/each}
+2
View File
@@ -199,6 +199,8 @@ menu.player-settings {
padding: 1rem;
font-size: 1rem;
border-radius: var(--border-radius);
overflow-y: scroll;
max-height: 200px;
}
[data-melt-combobox-option] {