mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-10-30 23:41:57 +01:00
feat: add searchable property to choice fields (#1061)
Adds an opt-in `searchable` option on `choice` fields which replaces the dropdown toggle with a filterable input, similar to the taxonomy field. Filtering matches on both the choice title and ID. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
639cc2d622
commit
f27093a638
8 files changed
+84
-12
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# Change Log
|
||||
|
||||
## [10.13.0] - 2026-xx-xx
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#1061](https://github.com/estruyf/vscode-front-matter/issues/1061): Added the `searchable` property on `choice` fields to show a searchable input instead of a dropdown, useful when you have many choices
|
||||
|
||||
## [10.12.0] - 2026-08-21 - [Release notes](https://beta.frontmatter.codes/updates/v10.12.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
@@ -429,6 +429,7 @@
|
||||
|
||||
"panel.fields.choiceField.select": "Select {0}",
|
||||
"panel.fields.choiceField.clear": "Clear value",
|
||||
"panel.fields.choiceField.search": "Search {0}",
|
||||
|
||||
"panel.fields.contentTypeRelationshipField.loading": "Fetching possible values...",
|
||||
|
||||
|
||||
@@ -1488,6 +1488,11 @@
|
||||
"default": false,
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.multiple.description%"
|
||||
},
|
||||
"searchable": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.searchable.description%"
|
||||
},
|
||||
"isPreviewImage": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description": "Is a single line field",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "Is a WYSIWYG field. You can set it to markdown or HTML.",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.multiple.description": "Do you allow to select multiple values?",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.searchable.description": "Show a searchable input for the choice field instead of a dropdown. Useful when you have many choices.",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isPreviewImage.description": "Specify if the image field can be used as preview. Be aware, you can only have one preview image per content type.",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.hidden.description": "Do you want to hide the field from the metadata section?",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description": "The ID of your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.",
|
||||
|
||||
@@ -1424,6 +1424,10 @@ export enum LocalizationKey {
|
||||
* Clear value
|
||||
*/
|
||||
panelFieldsChoiceFieldClear = 'panel.fields.choiceField.clear',
|
||||
/**
|
||||
* Search {0}
|
||||
*/
|
||||
panelFieldsChoiceFieldSearch = 'panel.fields.choiceField.search',
|
||||
/**
|
||||
* Fetching possible values...
|
||||
*/
|
||||
|
||||
@@ -106,6 +106,7 @@ export interface Field {
|
||||
single?: boolean;
|
||||
wysiwyg?: boolean | string;
|
||||
multiple?: boolean;
|
||||
searchable?: boolean;
|
||||
isPreviewImage?: boolean;
|
||||
hidden?: boolean;
|
||||
taxonomyId?: string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import useDropdownStyle from '../../hooks/useDropdownStyle';
|
||||
export interface IChoiceFieldProps extends BaseFieldProps<string | string[]> {
|
||||
choices: string[] | Choice[];
|
||||
multiSelect?: boolean;
|
||||
searchable?: boolean;
|
||||
onChange: (value: string | string[]) => void;
|
||||
}
|
||||
|
||||
@@ -23,10 +24,12 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({
|
||||
value,
|
||||
choices,
|
||||
multiSelect,
|
||||
searchable,
|
||||
onChange,
|
||||
required
|
||||
}: React.PropsWithChildren<IChoiceFieldProps>) => {
|
||||
const [crntSelected, setCrntSelected] = React.useState<string | string[] | null>(value);
|
||||
const [inputValue, setInputValue] = React.useState<string>('');
|
||||
const dsRef = React.useRef<Downshift<string> | null>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const { getDropdownStyle } = useDropdownStyle(inputRef as any);
|
||||
@@ -84,6 +87,18 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Filters the choices by the typed search value (matches on title and ID)
|
||||
*/
|
||||
const search = searchable ? (inputValue || '').toLowerCase() : '';
|
||||
const filteredChoices = !search
|
||||
? (availableChoices as Array<string | Choice>)
|
||||
: (availableChoices as Array<string | Choice>).filter(
|
||||
(choice) =>
|
||||
getValue(choice, 'title').toLowerCase().includes(search) ||
|
||||
getValue(choice, 'id').toLowerCase().includes(search)
|
||||
);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
return (
|
||||
required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected)
|
||||
@@ -102,24 +117,62 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({
|
||||
|
||||
<Downshift
|
||||
ref={dsRef}
|
||||
onSelect={(selected) => onValueChange(selected || '')}
|
||||
onSelect={(selected) => {
|
||||
onValueChange(selected || '');
|
||||
setInputValue('');
|
||||
}}
|
||||
itemToString={(item) => (item ? item : '')}
|
||||
inputValue={inputValue}
|
||||
onStateChange={(changes) => {
|
||||
// Only track what the user types, ignore the value Downshift sets on selection
|
||||
if (changes.type === Downshift.stateChangeTypes.changeInput) {
|
||||
setInputValue(changes.inputValue || '');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
|
||||
{({
|
||||
getToggleButtonProps,
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
getMenuProps,
|
||||
isOpen,
|
||||
getRootProps,
|
||||
openMenu,
|
||||
closeMenu
|
||||
}) => (
|
||||
<div
|
||||
{...getRootProps(undefined, { suppressRefError: true })}
|
||||
ref={inputRef}
|
||||
className={`metadata_field__choice`}
|
||||
>
|
||||
<button
|
||||
{...getToggleButtonProps({
|
||||
className: `metadata_field__choice__toggle`,
|
||||
disabled: availableChoices.length === 0
|
||||
})}
|
||||
>
|
||||
<span>{l10n.t(LocalizationKey.panelFieldsChoiceFieldSelect, label)}</span>
|
||||
<ChevronDownIcon className="icon" />
|
||||
</button>
|
||||
{searchable ? (
|
||||
<div
|
||||
className={`article__tags__input metadata_field__choice__search ${showRequiredState ? 'required' : ''}`}
|
||||
>
|
||||
<input
|
||||
{...getInputProps({
|
||||
onFocus: openMenu as () => void,
|
||||
onClick: openMenu as () => void,
|
||||
onBlur: () => {
|
||||
closeMenu();
|
||||
setInputValue('');
|
||||
},
|
||||
disabled: availableChoices.length === 0
|
||||
})}
|
||||
placeholder={l10n.t(LocalizationKey.panelFieldsChoiceFieldSearch, label)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
{...getToggleButtonProps({
|
||||
className: `metadata_field__choice__toggle`,
|
||||
disabled: availableChoices.length === 0
|
||||
})}
|
||||
>
|
||||
<span>{l10n.t(LocalizationKey.panelFieldsChoiceFieldSelect, label)}</span>
|
||||
<ChevronDownIcon className="icon" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ul
|
||||
className={`field_dropdown metadata_field__choice_list ${isOpen ? 'open' : 'closed'}`}
|
||||
@@ -129,7 +182,7 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({
|
||||
{...getMenuProps()}
|
||||
>
|
||||
{
|
||||
availableChoices.map((choice, index) => (
|
||||
filteredChoices.map((choice, index) => (
|
||||
<li
|
||||
{...getItemProps({
|
||||
key: getValue(choice, 'id'),
|
||||
|
||||
@@ -326,6 +326,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
required={!!field.required}
|
||||
choices={choices}
|
||||
multiSelect={field.multiple}
|
||||
searchable={!!field.searchable}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
</FieldBoundary>
|
||||
|
||||
Reference in new issue
Block a user