mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Merge branch 'issue/865' into beta
This commit is contained in:
5675
package-lock.json
generated
5675
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1366,7 +1366,14 @@
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description%"
|
||||
},
|
||||
"wysiwyg": {
|
||||
"type": "boolean",
|
||||
"type": [
|
||||
"boolean",
|
||||
"string"
|
||||
],
|
||||
"enum": [
|
||||
"html",
|
||||
"markdown"
|
||||
],
|
||||
"default": false,
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description%"
|
||||
},
|
||||
@@ -2887,7 +2894,14 @@
|
||||
"react-router-dom": "^6.8.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"recoil": "^0.7.7",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-remark": "^10.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.8",
|
||||
"simple-git": "^3.16.0",
|
||||
@@ -2897,6 +2911,7 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.5",
|
||||
"unified": "^11.0.5",
|
||||
"uniforms": "^3.10.2",
|
||||
"uniforms-antd": "^3.10.2",
|
||||
"uniforms-bridge-json-schema": "^3.10.2",
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.id.description": "The choice ID",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.title.description": "The choice title",
|
||||
"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 (HTML output)",
|
||||
"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.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?",
|
||||
|
||||
@@ -103,7 +103,7 @@ export interface Field {
|
||||
type: FieldType;
|
||||
choices?: string[] | Choice[];
|
||||
single?: boolean;
|
||||
wysiwyg?: boolean;
|
||||
wysiwyg?: boolean | string;
|
||||
multiple?: boolean;
|
||||
isPreviewImage?: boolean;
|
||||
hidden?: boolean;
|
||||
|
||||
@@ -16,7 +16,6 @@ const DEBOUNCE_TIME = 300;
|
||||
|
||||
export interface ITextFieldProps extends BaseFieldProps<string> {
|
||||
singleLine: boolean | undefined;
|
||||
wysiwyg: boolean | undefined;
|
||||
limit: number | undefined;
|
||||
rows?: number;
|
||||
name: string;
|
||||
@@ -26,12 +25,9 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
|
||||
onChange: (txtValue: string) => void;
|
||||
}
|
||||
|
||||
const WysiwygField = React.lazy(() => import('./WysiwygField'));
|
||||
|
||||
export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
placeholder,
|
||||
singleLine,
|
||||
wysiwyg,
|
||||
limit,
|
||||
label,
|
||||
description,
|
||||
@@ -199,13 +195,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{wysiwyg ? (
|
||||
<React.Suspense
|
||||
fallback={<div>{localize(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
|
||||
>
|
||||
<WysiwygField text={text || ''} onChange={onTextChange} />
|
||||
</React.Suspense>
|
||||
) : singleLine ? (
|
||||
{singleLine ? (
|
||||
<input
|
||||
className={`metadata_field__input`}
|
||||
value={text || ''}
|
||||
|
||||
@@ -27,12 +27,14 @@ import {
|
||||
NumberField,
|
||||
CustomField,
|
||||
FieldCollection,
|
||||
TagPicker
|
||||
TagPicker,
|
||||
WYSIWYGType
|
||||
} from '.';
|
||||
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
|
||||
import { ContentTypeRelationshipField } from './ContentTypeRelationshipField';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { LocalizationKey, localize } from '../../../localization';
|
||||
import { GeneralCommands } from '../../../constants';
|
||||
const WysiwygField = React.lazy(() => import('./WysiwygField'));
|
||||
|
||||
export interface IWrapperFieldProps {
|
||||
field: Field;
|
||||
@@ -210,24 +212,43 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
limit = settings?.seo.description;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<TextField
|
||||
name={field.name}
|
||||
label={field.title || field.name}
|
||||
description={field.description}
|
||||
singleLine={field.single}
|
||||
limit={limit}
|
||||
wysiwyg={field.wysiwyg}
|
||||
rows={4}
|
||||
onChange={onFieldChange}
|
||||
value={(fieldValue as string) || null}
|
||||
required={!!field.required}
|
||||
settings={settings}
|
||||
actions={field.actions}
|
||||
/>
|
||||
</FieldBoundary>
|
||||
);
|
||||
if (field.wysiwyg) {
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<React.Suspense
|
||||
fallback={<div>{localize(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
|
||||
>
|
||||
<WysiwygField
|
||||
name={field.name}
|
||||
label={field.title || field.name}
|
||||
description={field.description}
|
||||
limit={limit}
|
||||
type={typeof field.wysiwyg === 'boolean' ? 'html' : field.wysiwyg.toLowerCase() as WYSIWYGType}
|
||||
onChange={onFieldChange}
|
||||
value={(fieldValue as string) || null}
|
||||
required={!!field.required} />
|
||||
</React.Suspense>
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<TextField
|
||||
name={field.name}
|
||||
label={field.title || field.name}
|
||||
description={field.description}
|
||||
singleLine={field.single}
|
||||
limit={limit}
|
||||
rows={4}
|
||||
onChange={onFieldChange}
|
||||
value={(fieldValue as string) || null}
|
||||
required={!!field.required}
|
||||
settings={settings}
|
||||
actions={field.actions}
|
||||
/>
|
||||
</FieldBoundary>
|
||||
);
|
||||
}
|
||||
} else if (field.type === 'number') {
|
||||
let nrValue: number | null = field.numberOptions?.isDecimal ? parseFloat(fieldValue as string) : parseInt(fieldValue as string);
|
||||
if (isNaN(nrValue)) {
|
||||
@@ -556,7 +577,10 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else {
|
||||
console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type));
|
||||
messageHandler.send(GeneralCommands.toVSCode.logging.verbose, {
|
||||
message: localize(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type),
|
||||
location: 'PANEL'
|
||||
});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,25 +3,186 @@ import * as React from 'react';
|
||||
const ReactQuill = require('react-quill');
|
||||
import 'react-quill/dist/quill.snow.css';
|
||||
|
||||
export interface IWysiwygFieldProps {
|
||||
text: string;
|
||||
import { PencilIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import { unified } from "unified";
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import remarkStringify from "remark-stringify";
|
||||
|
||||
import rehypeParse from "rehype-parse";
|
||||
import rehypeRemark from "rehype-remark";
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
|
||||
import { differenceInSeconds } from "date-fns";
|
||||
import { BaseFieldProps } from '../../../models';
|
||||
import { FieldMessage, FieldTitle } from '.';
|
||||
import { LocalizationKey, localize } from '../../../localization';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { RequiredFieldsAtom } from '../../state';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
|
||||
const DEBOUNCE_TIME = 500;
|
||||
|
||||
function markdownToHtml(markdownText: string) {
|
||||
const file = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkRehype)
|
||||
.use(remarkGfm)
|
||||
.use(rehypeStringify)
|
||||
.processSync(markdownText);
|
||||
|
||||
const htmlContents = String(file);
|
||||
return htmlContents.replace(/<del>/g, '<s>').replace(/<\/del>/g, '</s>');
|
||||
}
|
||||
|
||||
function htmlToMarkdown(htmlText: string) {
|
||||
const file = unified()
|
||||
.use(rehypeParse, { emitParseErrors: true, duplicateAttribute: false })
|
||||
.use(rehypeRemark)
|
||||
.use(remarkGfm)
|
||||
.use(remarkStringify)
|
||||
.processSync(htmlText);
|
||||
return String(file);
|
||||
}
|
||||
|
||||
export type WYSIWYGType = "html" | "markdown";
|
||||
|
||||
export interface IWysiwygFieldProps extends BaseFieldProps<string> {
|
||||
name: string;
|
||||
limit?: number;
|
||||
type: WYSIWYGType;
|
||||
onChange: (txtValue: string) => void;
|
||||
}
|
||||
|
||||
const WysiwygField: React.FunctionComponent<IWysiwygFieldProps> = ({
|
||||
text,
|
||||
onChange
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
type = "html",
|
||||
onChange,
|
||||
limit,
|
||||
required
|
||||
}: React.PropsWithChildren<IWysiwygFieldProps>) => {
|
||||
const modules = {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['clean']
|
||||
]
|
||||
};
|
||||
const [, setRequiredFields] = useRecoilState(RequiredFieldsAtom);
|
||||
const [lastUpdated, setLastUpdated] = React.useState<number | null>(null);
|
||||
const [text, setText] = React.useState<string | null | undefined>(type === "html" ? value : markdownToHtml(value || ""));
|
||||
const debouncedText = useDebounce<string | null | undefined>(text, DEBOUNCE_TIME);
|
||||
|
||||
return <ReactQuill modules={modules} value={text || ''} onChange={onChange} />;
|
||||
const onTextChange = (newValue: string) => {
|
||||
setText(newValue);
|
||||
setLastUpdated(Date.now());
|
||||
}
|
||||
|
||||
const modules = React.useMemo(() => {
|
||||
const styles = ['bold', 'italic', 'strike'];
|
||||
|
||||
if (type === "html") {
|
||||
styles.push('underline');
|
||||
}
|
||||
|
||||
return {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
styles,
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['clean']
|
||||
]
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
const isValid = React.useMemo(() => {
|
||||
let temp = true;
|
||||
if (limit && limit !== -1) {
|
||||
temp = (text || '').length <= limit;
|
||||
}
|
||||
return temp;
|
||||
}, [limit, text]);
|
||||
|
||||
const updateRequired = React.useCallback(
|
||||
(isValid: boolean) => {
|
||||
setRequiredFields((prev) => {
|
||||
let clone = Object.assign([], prev);
|
||||
|
||||
if (isValid) {
|
||||
clone = clone.filter((item) => item !== label);
|
||||
} else {
|
||||
clone.push(label);
|
||||
}
|
||||
|
||||
return clone;
|
||||
});
|
||||
},
|
||||
[setRequiredFields]
|
||||
);
|
||||
|
||||
const showRequiredState = React.useMemo(() => {
|
||||
return required && !text;
|
||||
}, [required, text]);
|
||||
|
||||
const border = React.useMemo(() => {
|
||||
if (showRequiredState) {
|
||||
updateRequired(false);
|
||||
return '1px solid var(--vscode-inputValidation-errorBorder)';
|
||||
} else if (!isValid) {
|
||||
updateRequired(true);
|
||||
return '1px solid var(--vscode-inputValidation-warningBorder)';
|
||||
} else {
|
||||
updateRequired(true);
|
||||
return '1px solid var(--vscode-inputValidation-infoBorder)';
|
||||
}
|
||||
}, [showRequiredState, isValid]);
|
||||
|
||||
/**
|
||||
* Update the text value when the value changes
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (text !== value && (lastUpdated === null || differenceInSeconds(Date.now(), lastUpdated) > 2)) {
|
||||
setText(type === "html" ? value : markdownToHtml(value || ""));
|
||||
}
|
||||
setLastUpdated(null);
|
||||
}, [value]);
|
||||
|
||||
/**
|
||||
* Update the value when the debounced text changes
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (debouncedText !== undefined && value !== debouncedText && lastUpdated !== null) {
|
||||
const valueToUpdate = type === "html" ? debouncedText : htmlToMarkdown(debouncedText || "");
|
||||
onChange(valueToUpdate || "");
|
||||
}
|
||||
}, [debouncedText]);
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<FieldTitle
|
||||
label={label}
|
||||
icon={<PencilIcon />}
|
||||
required={required}
|
||||
/>
|
||||
|
||||
<ReactQuill
|
||||
modules={modules}
|
||||
value={text || ''}
|
||||
onChange={onTextChange}
|
||||
style={{ border }}
|
||||
/>
|
||||
|
||||
{limit && limit > 0 && (text || '').length > limit && (
|
||||
<div className={`metadata_field__limit`}>
|
||||
{localize(LocalizationKey.panelFieldsTextFieldLimit, `${(text || '').length}/${limit}`)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FieldMessage
|
||||
description={description}
|
||||
name={label.toLowerCase()}
|
||||
showRequired={showRequiredState}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WysiwygField;
|
||||
|
||||
@@ -991,7 +991,11 @@ vscode-divider {
|
||||
/* Quill changes */
|
||||
.ql-toolbar.ql-snow,
|
||||
.ql-container.ql-snow {
|
||||
border-color: var(--vscode-inputValidation-infoBorder) !important;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
border-bottom: 1px solid var(--vscode-inputValidation-infoBorder);
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
@@ -1023,6 +1027,7 @@ vscode-divider {
|
||||
.ql-editor {
|
||||
background-color: var(--vscode-input-background) !important;
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal) !important;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.ql-snow.ql-toolbar button:hover,
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
const path = require('path');
|
||||
const {
|
||||
ProvidePlugin
|
||||
} = require('webpack');
|
||||
const { ProvidePlugin } = require('webpack');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const WebpackManifestPlugin = require('webpack-manifest-plugin').WebpackManifestPlugin;
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
const path = require('path');
|
||||
const { ProvidePlugin } = require('webpack');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const WebpackManifestPlugin = require('webpack-manifest-plugin').WebpackManifestPlugin;
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
@@ -19,7 +20,7 @@ const config = [{
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx'],
|
||||
fallback: {
|
||||
"path": require.resolve("path-browserify")
|
||||
"path": require.resolve("path-browserify"),
|
||||
}
|
||||
},
|
||||
module: {
|
||||
@@ -69,7 +70,13 @@ const config = [{
|
||||
extensions: ['ts', 'tsx'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
emitWarning: false,
|
||||
})
|
||||
}),
|
||||
new ProvidePlugin({
|
||||
// Make a global `process` variable that points to the `process` package,
|
||||
// because the `util` package expects there to be a global variable named `process`.
|
||||
// Thanks to https://stackoverflow.com/a/65018686/14239942
|
||||
process: 'process/browser'
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
compress: true,
|
||||
|
||||
Reference in New Issue
Block a user