diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6b84d2..5b53b441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ ### 🎨 Enhancements - [#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 diff --git a/assets/media/styles.css b/assets/media/styles.css index 9a049c43..8c776498 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -452,17 +452,92 @@ input:checked + .field__toggle__slider:before { outline: none !important; } -.metadata_field__choice { +.metadata_field__choice__toggle { border: 1px solid var(--vscode-inputValidation-infoBorder) !important; outline: none !important; width: 100%; padding: var(--input-padding-vertical) var(--input-padding-horizontal); color: var(--vscode-input-foreground); background-color: var(--vscode-input-background); + + display: flex; + align-items: center; + position: relative; } -.metadata_field__choice::placeholde { - color: var(--vscode-input-placeholderForeground); +.metadata_field__choice__toggle:hover, +.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 { diff --git a/package.json b/package.json index fbfff88f..ca3e8073 100644 --- a/package.json +++ b/package.json @@ -268,12 +268,29 @@ "type": "array", "description": "Define your choices", "items": { - "type": "string" + "type": [ + "object", + "string" + ], + "properties": { + "id": { + "type": ["null", "string"], + "description": "The choice ID" + }, + "title": { + "type": "string", + "description": "The choice title" + } + } } }, "single": { "type": "boolean", "description": "Is a single line field" + }, + "multiSelect": { + "type": "boolean", + "description": "Do you allow to select multiple values?" } }, "additionalProperties": false, diff --git a/src/models/Choice.ts b/src/models/Choice.ts new file mode 100644 index 00000000..46d1be05 --- /dev/null +++ b/src/models/Choice.ts @@ -0,0 +1,5 @@ + +export interface Choice { + id: string; + title: string; +} \ No newline at end of file diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index e4471b1e..aadb9a49 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -1,4 +1,5 @@ import { FileType } from "vscode"; +import { Choice } from "./Choice"; import { DashboardData } from "./DashboardData"; export interface PanelSettings { @@ -27,8 +28,9 @@ export interface Field { title?: string; name: string; type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories"; - choices?: string[]; + choices?: string[] | Choice[]; single?: boolean; + multiSelect?: boolean; } export interface DateInfo { diff --git a/src/panelWebView/components/Fields/ChoiceButton.tsx b/src/panelWebView/components/Fields/ChoiceButton.tsx new file mode 100644 index 00000000..aee6cc25 --- /dev/null +++ b/src/panelWebView/components/Fields/ChoiceButton.tsx @@ -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 = ({title, value, onClick}: React.PropsWithChildren) => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/ChoiceField.tsx b/src/panelWebView/components/Fields/ChoiceField.tsx index 85bd3a6e..377a86b5 100644 --- a/src/panelWebView/components/Fields/ChoiceField.tsx +++ b/src/panelWebView/components/Fields/ChoiceField.tsx @@ -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 { useEffect } from 'react'; +import { Choice } from '../../../models/Choice'; import { VsLabel } from '../VscodeComponents'; +import { ChoiceButton } from './ChoiceButton'; export interface IChoiceFieldProps { label: string; - selected: string; - choices: string[]; - onChange: (value: string) => void; + selected: string | string[]; + choices: string[] | Choice[]; + multiSelect?: boolean; + onChange: (value: string | string[]) => void; } -export const ChoiceField: React.FunctionComponent = ({label, selected, choices, onChange}: React.PropsWithChildren) => { - const [ crntSelected, setCrntSelected ] = React.useState(selected); +export const ChoiceField: React.FunctionComponent = ({label, selected, choices, multiSelect, onChange}: React.PropsWithChildren) => { + const [ crntSelected, setCrntSelected ] = React.useState(selected); + const dsRef = React.useRef | null>(null); const onValueChange = (txtValue: string) => { - setCrntSelected(txtValue); - onChange(txtValue); + if (multiSelect) { + 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).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).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 (
@@ -26,17 +80,48 @@ export const ChoiceField: React.FunctionComponent = ({label, {label}
- - + + onValueChange(selected || "")} + itemToString={item => (item ? item : '')}> + {({ getToggleButtonProps, getItemProps, getMenuProps, isOpen, getRootProps }) => ( +
+ + +
    + { + isOpen ? availableChoices.map((choice, index) => ( +
  • + { getValue(choice, 'title') || Clear value } +
  • + )) : null + } +
+
+ )} +
+ + { + crntSelected instanceof Array ? crntSelected.map((value: string) => ( + + )) : ( + crntSelected && ( + + ) + ) + } ); }; \ No newline at end of file diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index ac65b556..d81c0c3b 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -139,6 +139,7 @@ export const Metadata: React.FunctionComponent = ({settings, met label={field.title || field.name} selected={choiceValue as string} choices={choices} + multiSelect={field.multiSelect} onChange={(value => sendUpdate(field.name, value))} /> ); } else if (field.type === 'tags') {