#119 #121 - Choice field enhancements

This commit is contained in:
Elio Struyf
2021-09-30 09:34:00 +02:00
parent f5e7526fae
commit 9eaf94de7a
8 changed files with 236 additions and 25 deletions
+6
View File
@@ -9,6 +9,12 @@
### 🎨 Enhancements ### 🎨 Enhancements
- [#117](https://github.com/estruyf/vscode-front-matter/issues/117): Allow to specify a singleline of text in the metadata fields - [#117](https://github.com/estruyf/vscode-front-matter/issues/117): Allow to specify a singleline of text in the metadata fields
- [#119](https://github.com/estruyf/vscode-front-matter/issues/119): Multi-select support for choice fields
- [#121](https://github.com/estruyf/vscode-front-matter/issues/121): Choice fields support ID/title objects as well as a regular string
### 🐞 Fixes
- [#120](https://github.com/estruyf/vscode-front-matter/issues/120): Choice and number field not updating when set manually in front matter of the file
## [4.0.1] - 2021-09-24 ## [4.0.1] - 2021-09-24
+78 -3
View File
@@ -452,17 +452,92 @@ input:checked + .field__toggle__slider:before {
outline: none !important; outline: none !important;
} }
.metadata_field__choice { .metadata_field__choice__toggle {
border: 1px solid var(--vscode-inputValidation-infoBorder) !important; border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
outline: none !important; outline: none !important;
width: 100%; width: 100%;
padding: var(--input-padding-vertical) var(--input-padding-horizontal); padding: var(--input-padding-vertical) var(--input-padding-horizontal);
color: var(--vscode-input-foreground); color: var(--vscode-input-foreground);
background-color: var(--vscode-input-background); background-color: var(--vscode-input-background);
display: flex;
align-items: center;
position: relative;
} }
.metadata_field__choice::placeholde { .metadata_field__choice__toggle:hover,
color: var(--vscode-input-placeholderForeground); .metadata_field__choice__toggle:focus,
.metadata_field__choice__toggle:active,
.metadata_field__choice__toggle:disabled {
background-color: var(--vscode-input-background);
}
.metadata_field__choice__toggle span {
margin-right: 1rem;
}
.metadata_field__choice__toggle svg.icon {
height: 1rem;
width: 1rem;
margin-left: .25rem;
position: absolute;
right: .25rem;
}
.metadata_field__choice_list {
width: 90%;
margin: 0;
padding: 0;
z-index: 1;
position: absolute;
list-style: none;
overflow: auto;
max-height: 200px;
color: var(--vscode-dropdown-foreground);
background-color: var(--vscode-dropdown-background);
}
.metadata_field__choice_list.open {
border: 1px solid rgba(0, 0, 0, .9);
}
.metadata_field__choice_list li {
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
cursor: pointer;
}
.metadata_field__choice_list li:active {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-background);
}
.metadata_field__choice_list li[aria-selected="true"] {
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-hoverBackground);
}
.metadata_field__choice_list li[aria-disabled="true"] {
display: none;
}
.metadata_field__choice_list__item {
opacity: 0.8;
}
.metadata_field__choice__button {
margin-top: .5rem;
display: inline-flex;
align-items: center;
width: auto;
margin-right: .5rem;
}
.metadata_field__choice__button_icon {
height: 1.25rem;
width: 1.25rem;
margin-left: .5rem;
} }
.metadata_field__datetime { .metadata_field__datetime {
+18 -1
View File
@@ -268,12 +268,29 @@
"type": "array", "type": "array",
"description": "Define your choices", "description": "Define your choices",
"items": { "items": {
"type": "string" "type": [
"object",
"string"
],
"properties": {
"id": {
"type": ["null", "string"],
"description": "The choice ID"
},
"title": {
"type": "string",
"description": "The choice title"
}
}
} }
}, },
"single": { "single": {
"type": "boolean", "type": "boolean",
"description": "Is a single line field" "description": "Is a single line field"
},
"multiSelect": {
"type": "boolean",
"description": "Do you allow to select multiple values?"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
+5
View File
@@ -0,0 +1,5 @@
export interface Choice {
id: string;
title: string;
}
+3 -1
View File
@@ -1,4 +1,5 @@
import { FileType } from "vscode"; import { FileType } from "vscode";
import { Choice } from "./Choice";
import { DashboardData } from "./DashboardData"; import { DashboardData } from "./DashboardData";
export interface PanelSettings { export interface PanelSettings {
@@ -27,8 +28,9 @@ export interface Field {
title?: string; title?: string;
name: string; name: string;
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories"; type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories";
choices?: string[]; choices?: string[] | Choice[];
single?: boolean; single?: boolean;
multiSelect?: boolean;
} }
export interface DateInfo { export interface DateInfo {
@@ -0,0 +1,20 @@
import { XIcon } from '@heroicons/react/outline';
import * as React from 'react';
export interface IChoiceButtonProps {
title: string;
value: string;
onClick: (value: string) => void;
}
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({title, value, onClick}: React.PropsWithChildren<IChoiceButtonProps>) => {
return (
<button
title={`Remove ${title}`}
className="metadata_field__choice__button"
onClick={() => onClick(value)}>
{title}
<XIcon className={`metadata_field__choice__button_icon`} />
</button>
);
};
@@ -1,24 +1,78 @@
import { CheckIcon } from '@heroicons/react/outline'; import { CheckIcon, ChevronDownIcon } from '@heroicons/react/outline';
import Downshift from 'downshift';
import * as React from 'react'; import * as React from 'react';
import { useEffect } from 'react';
import { Choice } from '../../../models/Choice';
import { VsLabel } from '../VscodeComponents'; import { VsLabel } from '../VscodeComponents';
import { ChoiceButton } from './ChoiceButton';
export interface IChoiceFieldProps { export interface IChoiceFieldProps {
label: string; label: string;
selected: string; selected: string | string[];
choices: string[]; choices: string[] | Choice[];
onChange: (value: string) => void; multiSelect?: boolean;
onChange: (value: string | string[]) => void;
} }
export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label, selected, choices, onChange}: React.PropsWithChildren<IChoiceFieldProps>) => { export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label, selected, choices, multiSelect, onChange}: React.PropsWithChildren<IChoiceFieldProps>) => {
const [ crntSelected, setCrntSelected ] = React.useState<string | null>(selected); const [ crntSelected, setCrntSelected ] = React.useState<string | string[] | null>(selected);
const dsRef = React.useRef<Downshift<string> | null>(null);
const onValueChange = (txtValue: string) => { const onValueChange = (txtValue: string) => {
setCrntSelected(txtValue); if (multiSelect) {
onChange(txtValue); const newValue = [...(crntSelected || []) as string[], txtValue];
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected(txtValue);
onChange(txtValue);
}
}; };
const containsSelected = crntSelected && choices.indexOf(crntSelected) !== -1; const removeSelected = (txtValue: string) => {
if (multiSelect) {
const newValue = [...(crntSelected || [])].filter(v => v !== txtValue);
setCrntSelected(newValue);
onChange(newValue);
} else {
setCrntSelected("");
onChange("");
}
};
const getValue = (value: string | Choice, type: "id" | "title") => {
if (typeof value === 'string' || typeof value === 'number') {
return `${value}`;
}
return `${value[type]}`;
};
const getChoiceValue = (value: string) => {
const choice = (choices as Array<string | Choice>).find((c: string | Choice) => getValue(c, 'id') === value);
if (choice) {
return getValue(choice, 'title');
}
return "";
};
useEffect(() => {
if (crntSelected !== selected) {
setCrntSelected(selected);
}
}, [selected]);
const availableChoices = !multiSelect ? choices : (choices as Array<string | Choice>).filter((choice: string | Choice) => {
const value = typeof choice === 'string' || typeof choice === 'number' ? choice : choice.id;
if (typeof crntSelected === 'string') {
return crntSelected !== `${value}`;
} else if (crntSelected instanceof Array) {
return crntSelected.indexOf(`${value}`) === -1;
}
return true;
});
return ( return (
<div className={`metadata_field`}> <div className={`metadata_field`}>
<VsLabel> <VsLabel>
@@ -26,17 +80,48 @@ export const ChoiceField: React.FunctionComponent<IChoiceFieldProps> = ({label,
<CheckIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span> <CheckIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div> </div>
</VsLabel> </VsLabel>
<select <Downshift
value={crntSelected || ""} ref={dsRef}
placeholder={`Select from your ${label}`} onChange={(selected) => onValueChange(selected || "")}
className={`metadata_field__choice`} itemToString={item => (item ? item : '')}>
onChange={(e) => onValueChange(e.currentTarget.value)}> {({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => (
{ !containsSelected && <option value='' disabled hidden></option> } <div {...getRootProps(undefined, {suppressRefError: true})} className={`metadata_field__choice`}>
{choices.map((choice, index) => ( <button
<option key={index} value={choice}>{choice}</option> {...getToggleButtonProps({
))} className: `metadata_field__choice__toggle`,
</select> disabled: availableChoices.length === 0
})}>
<span>{`Select your ${label} value`}</span>
<ChevronDownIcon className="icon" />
</button>
<ul className={`metadata_field__choice_list ${isOpen ? "open" : "closed" }`} {...getMenuProps()}>
{
isOpen ? availableChoices.map((choice, index) => (
<li {...getItemProps({
key: getValue(choice, 'id'),
index,
item: getValue(choice, 'id'),
})}>
{ getValue(choice, 'title') || <span className={`metadata_field__choice_list__item`}>Clear value</span> }
</li>
)) : null
}
</ul>
</div>
)}
</Downshift>
{
crntSelected instanceof Array ? crntSelected.map((value: string) => (
<ChoiceButton key={value} value={value} title={getChoiceValue(value)} onClick={removeSelected} />
)) : (
crntSelected && (
<ChoiceButton key={crntSelected} value={crntSelected} title={getChoiceValue(crntSelected)} onClick={removeSelected} />
)
)
}
</div> </div>
); );
}; };
+1
View File
@@ -139,6 +139,7 @@ export const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, met
label={field.title || field.name} label={field.title || field.name}
selected={choiceValue as string} selected={choiceValue as string}
choices={choices} choices={choices}
multiSelect={field.multiSelect}
onChange={(value => sendUpdate(field.name, value))} /> onChange={(value => sendUpdate(field.name, value))} />
); );
} else if (field.type === 'tags') { } else if (field.type === 'tags') {