diff --git a/.storybook/preview.ts b/.storybook/preview.ts index cdde3314..1b29ac4d 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -2,6 +2,14 @@ import {withTheme, withLang} from './decorators'; export const decorators = [withTheme, withLang]; +export const parameters = { + options: { + storySort: { + order: ['Docs', 'Array', 'Boolean', 'Other', 'Number', 'Object', 'String', '*'], + }, + }, +}; + export const globalTypes = { theme: { defaultValue: 'light', diff --git a/docs/lib.md b/docs/lib.md index cd17127d..c2f2315a 100644 --- a/docs/lib.md +++ b/docs/lib.md @@ -21,6 +21,7 @@ This component serves as the primary entry point for drawing dynamic forms. | destroyOnUnregister | `boolean` | | If true, the value of a field will be destroyed when that field is unregistered. Defaults to true | | generateRandomValue | `function` | | Function that is necessary to generate a random value | | storeSubscriber | `(storeValue: FieldValue) => void` | | Subscriber function will be called when internal store of dynamic field is changed | +| renderHtml | `function` | | Function for custom rendering of HTML and markdown content in descriptions | ### Controller @@ -48,6 +49,7 @@ This component serves as the primary entry point for creating an overview of for | Link | `React.ComponentType<{value: FormValue; link: Spec['viewSpec']['link'];}>` | | [Component](./spec.md#link) for converting values to links | | Monaco | `React.ComponentType` | | [MonacoEditor](https://github.com/react-monaco-editor/react-monaco-editor) component for Monaco [Input](./config.md#inputs) | | showLayoutDescription | boolean | | enable to show viewSpec.layoutDescription hint | +| renderHtml | `function` | | Function for custom rendering of HTML and markdown content in descriptions | ### ViewController diff --git a/docs/renderHtml.md b/docs/renderHtml.md new file mode 100644 index 00000000..0c9057e4 --- /dev/null +++ b/docs/renderHtml.md @@ -0,0 +1,52 @@ +# renderHtml + +## Description + +`renderHtml` is an optional property intended for custom rendering of HTML and markdown content in descriptions. +This property is useful when you need to render formatted text (markdown or HTML) keeping it consistent with your project's design and styling. + +Type of the property: + +```ts +renderHtml?: (text: string) => React.ReactNode; +``` + +--- + +## How to use renderHtml? + +To use this prop, pass your custom render function to the [DynamicField](./lib.md#dynamicfield) or [DynamicView](./lib.md#dynamicview) components. + +Example for [DynamicField](./lib.md#dynamicfield): + +```tsx +// Use any third-party library or your custom implementation for HTML rendering +const renderHtml = (text: string) => { + return
; +}; + +export const MyForm = () => { + return ; +}; +``` + +Example for [DynamicView](./lib.md#dynamicview): + +```tsx +const renderHtml = (text: string) => { + return
; +}; + +export const MyView = () => { + return ( + + ); +}; +``` + +--- + +### Tips for using renderHtml: + +- You can use any third-party libraries (for example, `@gravity-ui/markdown-editor`, `@diplodoc/transform`, etc.) to ensure safer and more effective rendering of markdown or HTML content. +- If your custom components (for instance, custom layout or custom input components) also require markdown or HTML rendering capabilities, be sure to leverage the built-in hook `useRenderHtml()` for convenient access to the rendering function within child components. diff --git a/package-lock.json b/package-lock.json index 09c6f1dc..fdc56bea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.0", "@types/lodash": "^4.14.180", + "@types/markdown-it": "^14.1.2", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/react-is": "^17.0.3", @@ -58,6 +59,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.5.0", "jest-transform-css": "^6.0.1", + "markdown-it": "^14.1.0", "monaco-editor": "^0.30.1", "monaco-editor-webpack-plugin": "^6.0.0", "npm-run-all": "^4.1.5", @@ -6385,12 +6387,37 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.14.191", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", "dev": true }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -7058,6 +7085,30 @@ } } }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -17125,6 +17176,16 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -17428,6 +17489,44 @@ "node": ">=0.10.0" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -17625,6 +17724,13 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", @@ -19527,6 +19633,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -22941,6 +23057,13 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -24076,6 +24199,23 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/webpack/node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", @@ -28729,12 +28869,34 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, "@types/lodash": { "version": "4.14.191", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", "dev": true }, + "@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "requires": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, "@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -36776,6 +36938,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -37032,6 +37203,34 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + } + } + }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -37194,6 +37393,12 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", @@ -38582,6 +38787,12 @@ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true + }, "pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -41069,6 +41280,12 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, "unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index 49e5b50a..44252394 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.0", "@types/lodash": "^4.14.180", + "@types/markdown-it": "^14.1.2", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/react-is": "^17.0.3", @@ -93,6 +94,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.5.0", "jest-transform-css": "^6.0.1", + "markdown-it": "^14.1.0", "monaco-editor": "^0.30.1", "monaco-editor-webpack-plugin": "^6.0.0", "npm-run-all": "^4.1.5", diff --git a/src/lib/core/components/Form/DynamicField.tsx b/src/lib/core/components/Form/DynamicField.tsx index 61a71f8c..6212e130 100644 --- a/src/lib/core/components/Form/DynamicField.tsx +++ b/src/lib/core/components/Form/DynamicField.tsx @@ -34,6 +34,7 @@ export interface DynamicFieldProps { destroyOnUnregister?: boolean; mutators?: DynamicFormMutators; shared?: Record; + renderHtml?: (text: string) => React.ReactNode; storeSubscriber?: (store: FieldValue) => void; __mirror?: WonderMirror; } @@ -49,6 +50,7 @@ export const DynamicField: React.FC = ({ destroyOnUnregister = true, mutators: externalMutators, shared: externalShared, + renderHtml, storeSubscriber, __mirror, }) => { @@ -74,6 +76,7 @@ export const DynamicField: React.FC = ({ store, shared, mutatorsStore, + renderHtml, __mirror, }), [ @@ -86,6 +89,7 @@ export const DynamicField: React.FC = ({ mutatorsStore, mutateDFState, store, + renderHtml, ], ); diff --git a/src/lib/core/components/Form/hooks/useRenderHtml.ts b/src/lib/core/components/Form/hooks/useRenderHtml.ts new file mode 100644 index 00000000..17b09037 --- /dev/null +++ b/src/lib/core/components/Form/hooks/useRenderHtml.ts @@ -0,0 +1,3 @@ +import {useDynamicFormsCtx} from './useDynamicFormsCtx'; + +export const useRenderHtml = () => useDynamicFormsCtx().renderHtml; diff --git a/src/lib/core/components/Form/types/context.ts b/src/lib/core/components/Form/types/context.ts index 1aa49f75..3ca51a1c 100644 --- a/src/lib/core/components/Form/types/context.ts +++ b/src/lib/core/components/Form/types/context.ts @@ -31,5 +31,6 @@ export interface DynamicFormsContext { onChangeShared: (name: string, value: any) => void; }; mutatorsStore: DynamicFormMutatorsStore; + renderHtml?: (text: string) => React.ReactNode; __mirror?: WonderMirror; } diff --git a/src/lib/core/components/View/DynamicView.tsx b/src/lib/core/components/View/DynamicView.tsx index 4096fd1a..eb3fb2dc 100644 --- a/src/lib/core/components/View/DynamicView.tsx +++ b/src/lib/core/components/View/DynamicView.tsx @@ -20,6 +20,7 @@ export interface DynamicViewProps { link: Spec['viewSpec']['link']; }>; Monaco?: React.ComponentType; + renderHtml?: (text: string) => React.ReactNode; showLayoutDescription?: boolean; shared?: Record; } @@ -30,6 +31,7 @@ export const DynamicView = ({ config, Link, Monaco, + renderHtml, showLayoutDescription, shared: externalShared, }: DynamicViewProps) => { @@ -44,8 +46,9 @@ export const DynamicView = ({ Link, Monaco: isValidElementType(Monaco) ? Monaco : undefined, shared, + renderHtml, }), - [config, value, Link, Monaco, showLayoutDescription, shared], + [config, value, Link, Monaco, showLayoutDescription, shared, renderHtml], ); if (isCorrectSpec(spec) && isCorrectViewConfig(config)) { diff --git a/src/lib/core/components/View/hooks/useRenderHtml.ts b/src/lib/core/components/View/hooks/useRenderHtml.ts new file mode 100644 index 00000000..17b09037 --- /dev/null +++ b/src/lib/core/components/View/hooks/useRenderHtml.ts @@ -0,0 +1,3 @@ +import {useDynamicFormsCtx} from './useDynamicFormsCtx'; + +export const useRenderHtml = () => useDynamicFormsCtx().renderHtml; diff --git a/src/lib/core/components/View/types/context.ts b/src/lib/core/components/View/types/context.ts index e4cf8b83..9bffedeb 100644 --- a/src/lib/core/components/View/types/context.ts +++ b/src/lib/core/components/View/types/context.ts @@ -15,6 +15,7 @@ export interface DynamicViewContext { link: Spec['viewSpec']['link']; }>; Monaco?: React.ComponentType; + renderHtml?: (text: string) => React.ReactNode; shared: { store: Record; onChangeShared: (name: string, value: any) => void; diff --git a/src/lib/kit/components/AccordeonCard/AccordeonCard.tsx b/src/lib/kit/components/AccordeonCard/AccordeonCard.tsx index 47f1512b..ebc42eae 100644 --- a/src/lib/kit/components/AccordeonCard/AccordeonCard.tsx +++ b/src/lib/kit/components/AccordeonCard/AccordeonCard.tsx @@ -5,6 +5,7 @@ import {Button, Icon, Text} from '@gravity-ui/uikit'; import isString from 'lodash/isString'; import {block} from '../../utils'; +import {HTMLContent} from '../HTMLContent'; import './AccordeonCard.scss'; @@ -95,9 +96,9 @@ export const AccordeonCard: React.FC = ({
{header} {description ? ( - ) : null}
diff --git a/src/lib/kit/components/HTMLContent/HTMLContent.tsx b/src/lib/kit/components/HTMLContent/HTMLContent.tsx index 318c11e7..8873c7b9 100644 --- a/src/lib/kit/components/HTMLContent/HTMLContent.tsx +++ b/src/lib/kit/components/HTMLContent/HTMLContent.tsx @@ -1,9 +1,29 @@ import React from 'react'; +import {isFunction} from 'lodash'; + +import {useRenderHtml as useRenderHtmlForm} from '../../../core/components/Form/hooks/useRenderHtml'; +import {useRenderHtml as useRenderHtmlView} from '../../../core/components/View/hooks/useRenderHtml'; + interface HTMLContentProps { html: string; + className?: string; } -export const HTMLContent: React.FC = ({html}) => { - return
; +export const HTMLContent: React.FC = ({html, className}) => { + const renderHtmlForm = useRenderHtmlForm(); + const renderHtmlView = useRenderHtmlView(); + + const content = React.useMemo(() => { + if (renderHtmlForm && isFunction(renderHtmlForm)) { + return renderHtmlForm(html); + } + if (renderHtmlView && isFunction(renderHtmlView)) { + return renderHtmlView(html); + } + + return
; + }, [className, html, renderHtmlForm, renderHtmlView]); + + return {content}; }; diff --git a/src/lib/kit/components/Inputs/TextContent/TextContent.tsx b/src/lib/kit/components/Inputs/TextContent/TextContent.tsx index d6bd6781..55be287f 100644 --- a/src/lib/kit/components/Inputs/TextContent/TextContent.tsx +++ b/src/lib/kit/components/Inputs/TextContent/TextContent.tsx @@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep'; import type {StringIndependentInput, StringSpec} from '../../../../core'; import {block} from '../../../utils'; +import {HTMLContent} from '../../HTMLContent'; import {LazyLoader} from '../../LazyLoader'; import {loadIcon} from './utils'; @@ -40,7 +41,7 @@ export const TextContentComponent: React.FC = ({ ) : undefined; - let content = ; + let content = ; if (textContentParams?.themeAlert) { const titleAlert = diff --git a/src/lib/kit/components/Layouts/Row/Row.tsx b/src/lib/kit/components/Layouts/Row/Row.tsx index 4345fa0e..b8c4b4a9 100644 --- a/src/lib/kit/components/Layouts/Row/Row.tsx +++ b/src/lib/kit/components/Layouts/Row/Row.tsx @@ -83,9 +83,9 @@ const RowBase = ({ ) : null}
{verboseDescription && spec.viewSpec.layoutDescription ? ( -
) : null}
diff --git a/src/lib/kit/components/Layouts/Section/Section.tsx b/src/lib/kit/components/Layouts/Section/Section.tsx index 84b2e673..c4c7d9bb 100644 --- a/src/lib/kit/components/Layouts/Section/Section.tsx +++ b/src/lib/kit/components/Layouts/Section/Section.tsx @@ -107,14 +107,7 @@ const SectionBase = < let description: React.ReactNode; if (spec.viewSpec.layoutDescription && !ignoreDescription) { if (descriptionAsSubtitle) { - description = ( -
- ); + description = ; } else { description = ( diff --git a/src/stories/CustomRenderHtml.mdx b/src/stories/CustomRenderHtml.mdx new file mode 100644 index 00000000..5ad5d76d --- /dev/null +++ b/src/stories/CustomRenderHtml.mdx @@ -0,0 +1,21 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; + +import Config from '../../docs/renderHtml.md?raw'; + + + +export const replacements = [ + ['../src', 'https://github.com/gravity-ui/dynamic-forms/blob/main/src/'], + ['./lib.md', '?path=/docs/docs-lib--docs'], + ['./spec.md', '?path=/docs/docs-spec--docs'], +]; + +export const applyReplacements = (text, replacements) => { + return replacements.reduce((result, [searchValue, replaceValue]) => { + return result.split(searchValue).join(replaceValue); + }, text); +}; + +export const content = applyReplacements(Config, replacements); + +{content} diff --git a/src/stories/CustomRenderHtml.stories.tsx b/src/stories/CustomRenderHtml.stories.tsx new file mode 100644 index 00000000..0f3de8eb --- /dev/null +++ b/src/stories/CustomRenderHtml.stories.tsx @@ -0,0 +1,104 @@ +import React from 'react'; + +import type {StoryFn} from '@storybook/react'; + +import type {ArrayBase, ObjectSpec} from '../lib'; +import {SpecTypes} from '../lib'; + +import {Editor as EditorBase} from './components/Editor/Editor'; + +export default { + title: 'Other/CustomRenderHtml', + component: EditorBase, +}; + +const spec: ObjectSpec = { + type: SpecTypes.Object, + required: true, + properties: { + accordeon: { + type: SpecTypes.Object, + required: true, + properties: { + row: { + type: SpecTypes.Boolean, + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'Row description md', + layoutDescription: '### Description is made in markdown format', + }, + }, + rowVerbose: { + type: SpecTypes.String, + viewSpec: { + type: 'base', + layout: 'row_verbose', + layoutTitle: 'Row Verbose', + placeholder: 'placeholder text', + layoutDescription: '`Row Verbose description md`', + }, + }, + alert: { + type: SpecTypes.String, + viewSpec: { + type: 'text_content', + textContentParams: { + text: '### Introduction to Web Technologies\n Markdown is **incredibly useful** for quick documentation. It allows you to: \n\n _Format text easily_\n\n -Create lists-\n\n -Add links

Important HTML Section

This section uses HTML for more complex formatting needs that Markdown doesnt support natively.

', + themeAlert: 'info', + icon: '', + }, + }, + }, + }, + viewSpec: { + type: 'base', + layout: 'accordeon', + layoutTitle: 'Accordeon', + layoutOpen: true, + layoutDescription: '### Description is made in markdown format', + }, + }, + card: { + type: SpecTypes.Object, + required: true, + properties: { + column: { + type: SpecTypes.String, + viewSpec: { + type: 'base', + layout: 'column', + layoutTitle: 'column', + placeholder: 'placeholder text', + layoutDescription: + '[Link html Dynamic Forms](https://github.com/gravity-ui/dynamic-forms)', + }, + }, + }, + viewSpec: { + type: 'base', + layout: 'card_section', + layoutTitle: 'Card', + layoutOpen: true, + layoutDescription: '### Description is made in markdown format', + }, + }, + }, + viewSpec: { + type: 'base', + layout: 'transparent', + layoutTitle: '', + layoutOpen: true, + layoutDescription: '', + }, +}; + +const template = () => { + const Template: StoryFn = (__, {viewMode}) => ( + + ); + + return Template; +}; + +export const CustomRenderHtml = template(); diff --git a/src/stories/components/DynamicField/DynamicField.tsx b/src/stories/components/DynamicField/DynamicField.tsx index 870da24f..57970ecd 100644 --- a/src/stories/components/DynamicField/DynamicField.tsx +++ b/src/stories/components/DynamicField/DynamicField.tsx @@ -9,11 +9,20 @@ import type {FieldValue, Spec, StringSpec} from '../../../lib'; import {DynamicField as BaseDynamicField, dynamicConfig, prepareSpec} from '../../../lib'; import {SpecSelector} from '../InputPreview/SpecSelector'; +const RenderHtmlAsync = React.lazy(() => import('../RenderHtml')); + +const renderHtml = (text: string) => ( + Loading...
}> + + +); + export interface DynamicFieldProps { name: string; spec: Spec; search?: string | ((spec: Spec, input: FieldValue, name: string) => boolean); parseJsonDefaultValue?: boolean; + withCustomRenderHtml?: boolean; } export const DynamicField: React.FC = ({ @@ -21,6 +30,7 @@ export const DynamicField: React.FC = ({ spec, search, parseJsonDefaultValue = true, + withCustomRenderHtml, }) => { const config = React.useMemo(() => { const cfg = cloneDeep(dynamicConfig); @@ -47,6 +57,7 @@ export const DynamicField: React.FC = ({ Monaco={MonacoEditor} search={search} generateRandomValue={generateRandomValue} + renderHtml={withCustomRenderHtml ? renderHtml : undefined} /> ); }; diff --git a/src/stories/components/DynamicView/DynamicView.tsx b/src/stories/components/DynamicView/DynamicView.tsx index c44ab9e2..6db8d1e5 100644 --- a/src/stories/components/DynamicView/DynamicView.tsx +++ b/src/stories/components/DynamicView/DynamicView.tsx @@ -7,19 +7,36 @@ import {DynamicView as BaseDynamicView, dynamicViewConfig, prepareSpec} from '.. import {DynLink} from './DynLink'; +const RenderHtmlAsync = React.lazy(() => import('../RenderHtml')); + +const renderHtml = (text: string) => ( + Loading...
}> + + +); + export interface DynamicViewProps { value: FormValue; spec: Spec; showLayoutDescription?: boolean; + withCustomRenderHtml?: boolean; } -export const DynamicView: React.FC = ({value, spec, showLayoutDescription}) => ( - -); +export const DynamicView: React.FC = ({ + value, + spec, + showLayoutDescription, + withCustomRenderHtml, +}) => { + return ( + + ); +}; diff --git a/src/stories/components/Editor/Editor.tsx b/src/stories/components/Editor/Editor.tsx index f2dd8798..cb2f9dfd 100644 --- a/src/stories/components/Editor/Editor.tsx +++ b/src/stories/components/Editor/Editor.tsx @@ -19,9 +19,15 @@ export interface EditorProps { spec: Spec; value?: FormValue; viewMode: 'story' | 'docs'; + withCustomRenderHtml?: boolean; } -export const Editor: React.FC = ({spec: externalSpec, value, viewMode}) => { +export const Editor: React.FC = ({ + spec: externalSpec, + value, + viewMode, + withCustomRenderHtml, +}) => { const [spec, setSpec] = React.useState(externalSpec); const [ready, setReady] = React.useState(true); const [showLayoutDescription, setShowLayoutDescription] = React.useState(false); @@ -150,6 +156,7 @@ export const Editor: React.FC = ({spec: externalSpec, value, viewMo name="input" spec={spec} parseJsonDefaultValue={parseJson} + withCustomRenderHtml={withCustomRenderHtml} />
) : null} @@ -170,6 +177,7 @@ export const Editor: React.FC = ({spec: externalSpec, value, viewMo spec, showLayoutDescription, )} + withCustomRenderHtml={withCustomRenderHtml} />
) : null} diff --git a/src/stories/components/RenderHtml.tsx b/src/stories/components/RenderHtml.tsx new file mode 100644 index 00000000..4f3a4743 --- /dev/null +++ b/src/stories/components/RenderHtml.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import MarkdownIt from 'markdown-it'; + +export const RenderHtml = ({text}: {text: string}) => { + const [html, setHtml] = React.useState(''); + + React.useEffect(() => { + const md = new MarkdownIt({html: true}); + setHtml(md.render(text)); + }, [text]); + + return ( + + {html ?
: null} + + ); +}; + +export default RenderHtml;