mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-06 17:02:54 +02:00
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
export interface Choice {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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') {
|
||||||
|
|||||||
Reference in New Issue
Block a user