Skip to content

Commit 1df544e

Browse files
committed
Add purifyOptions prop to <RichTextField>
1 parent cdd3995 commit 1df544e

5 files changed

Lines changed: 123 additions & 5 deletions

File tree

docs/RichTextField.md

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import { RichTextField } from 'react-admin';
2222

2323
## Props
2424

25-
| Prop | Required | Type | Default | Description |
26-
| ----------- | -------- | --------- | -------- | ---------------------------------------------------- |
27-
| `stripTags` | Optional | `boolean` | `false` | If `true`, remove all HTML tags and render text only |
25+
| Prop | Required | Type | Default | Description |
26+
| --------------- | -------- | --------- | -------- | --------------------------------------------------------------------- |
27+
| `stripTags` | Optional | `boolean` | `false` | If `true`, remove all HTML tags and render text only |
28+
| `purifyOptions` | Optional | `object` | - | The options passed to the DomPurify library when calling `sanitize()` |
2829

2930
`<RichTextField>` also accepts the [common field props](./Fields.md#common-field-props).
3031

@@ -37,3 +38,43 @@ import { RichTextField } from 'react-admin';
3738

3839
<RichTextField source="body" stripTags />
3940
```
41+
## `purifyOptions`
42+
43+
The `purifyOptions` prop allows to pass additional options to the DomPurify library when calling `sanitize()`.
44+
45+
For instance you can use the `ADD_ATTR` option to allow additional attributes, like `'target'`:
46+
47+
```jsx
48+
import { RichTextField } from 'react-admin';
49+
50+
<RichTextField source="body" purifyOptions={{ ADD_ATTR: ['target'] }} />
51+
```
52+
53+
**Tip:** More available options can be found in the [DomPurify Readme](https://github.com/cure53/DOMPurify#can-i-configure-dompurify).
54+
55+
## Open Links in a New Tab
56+
57+
If you wish to open all links in a new tab, you can use the following snippet to add the `target="_blank"` attribute to all links:
58+
59+
```jsx
60+
import { RichTextField, RichTextFieldProps } from 'react-admin';
61+
import dompurify from 'dompurify';
62+
63+
const TargetBlankEnabledRichTextField = (props: RichTextFieldProps) => {
64+
dompurify.addHook('afterSanitizeAttributes', function (node) {
65+
// set all elements owning target to target=_blank
66+
if ('target' in node) {
67+
node.setAttribute('target', '_blank');
68+
node.setAttribute('rel', 'noopener');
69+
}
70+
});
71+
return <RichTextField {...props} />;
72+
};
73+
74+
const MyComponent = () => (
75+
<TargetBlankEnabledRichTextField source="body" />
76+
);
77+
```
78+
79+
**Tip:** Note that this also adds the `rel="noopener"` attribute to all links, to prevent [reverse tabnabbing](https://mathiasbynens.github.io/rel-noopener/).
80+

packages/ra-ui-materialui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@mui/icons-material": "^5.0.1",
3030
"@mui/material": "^5.0.2",
3131
"@testing-library/react": "^11.2.3",
32+
"@types/dompurify": "^3.0.2",
3233
"cross-env": "^5.2.0",
3334
"expect": "^27.4.6",
3435
"file-api": "~0.10.4",

packages/ra-ui-materialui/src/field/RichTextField.stories.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { RecordContextProvider, useTimeout } from 'ra-core';
33
import dompurify from 'dompurify';
44

5-
import { RichTextField } from './RichTextField';
5+
import { RichTextField, RichTextFieldProps } from './RichTextField';
66
import { SimpleShowLayout } from '../detail/SimpleShowLayout';
77

88
export default {
@@ -82,3 +82,54 @@ It is regarded as one of Tolstoy's finest literary achievements and remains a cl
8282
</div>
8383
</RecordContextProvider>
8484
);
85+
86+
const TargetBlankEnabledRichTextField = (props: RichTextFieldProps) => {
87+
dompurify.addHook('afterSanitizeAttributes', function (node) {
88+
// set all elements owning target to target=_blank
89+
if ('target' in node) {
90+
node.setAttribute('target', '_blank');
91+
node.setAttribute('rel', 'noopener');
92+
}
93+
});
94+
return <RichTextField {...props} />;
95+
};
96+
97+
export const TargetBlank = () => (
98+
<RecordContextProvider
99+
value={{
100+
id: 1,
101+
body: `
102+
<p>
103+
<strong>War and Peace</strong> is a novel by the Russian author
104+
<a href="https://en.wikipedia.org/wiki/Leo_Tolstoy" target="_blank">Leo Tolstoy</a>,
105+
published serially, then in its entirety in 1869.
106+
</p>
107+
<p>
108+
It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature.
109+
</p>
110+
`,
111+
}}
112+
>
113+
<TargetBlankEnabledRichTextField source="body" />
114+
</RecordContextProvider>
115+
);
116+
117+
export const PurifyOptions = () => (
118+
<RecordContextProvider
119+
value={{
120+
id: 1,
121+
body: `
122+
<p>
123+
<strong>War and Peace</strong> is a novel by the Russian author
124+
<a href="https://en.wikipedia.org/wiki/Leo_Tolstoy" target="_blank">Leo Tolstoy</a>,
125+
published serially, then in its entirety in 1869.
126+
</p>
127+
<p>
128+
It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature.
129+
</p>
130+
`,
131+
}}
132+
>
133+
<RichTextField source="body" purifyOptions={{ ADD_ATTR: ['target'] }} />
134+
</RecordContextProvider>
135+
);

packages/ra-ui-materialui/src/field/RichTextField.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const RichTextField: FC<RichTextFieldProps> = memo<RichTextFieldProps>(
3131
emptyText,
3232
source,
3333
stripTags = false,
34+
purifyOptions,
3435
...rest
3536
} = props;
3637
const record = useRecordContext(props);
@@ -50,7 +51,7 @@ export const RichTextField: FC<RichTextFieldProps> = memo<RichTextFieldProps>(
5051
) : (
5152
<span
5253
dangerouslySetInnerHTML={{
53-
__html: purify.sanitize(value),
54+
__html: purify.sanitize(value, purifyOptions),
5455
}}
5556
/>
5657
)}
@@ -64,13 +65,20 @@ RichTextField.propTypes = {
6465
...Typography.propTypes,
6566
...fieldPropTypes,
6667
stripTags: PropTypes.bool,
68+
purifyOptions: PropTypes.any,
69+
};
70+
71+
export type PurifyOptions = purify.Config & {
72+
RETURN_DOM_FRAGMENT?: false | undefined;
73+
RETURN_DOM?: false | undefined;
6774
};
6875

6976
export interface RichTextFieldProps
7077
extends PublicFieldProps,
7178
InjectedFieldProps,
7279
Omit<TypographyProps, 'textAlign'> {
7380
stripTags?: boolean;
81+
purifyOptions?: PurifyOptions;
7482
}
7583

7684
RichTextField.displayName = 'RichTextField';

yarn.lock

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6018,6 +6018,15 @@ __metadata:
60186018
languageName: node
60196019
linkType: hard
60206020

6021+
"@types/dompurify@npm:^3.0.2":
6022+
version: 3.0.2
6023+
resolution: "@types/dompurify@npm:3.0.2"
6024+
dependencies:
6025+
"@types/trusted-types": "*"
6026+
checksum: 54cf82078d1eab075c75ccba252f08514963e29b8afd63e940ea33a9eefcbac86b43e169415ff82af11c128f03921e1e0ab4fdfaa65ce56d4cd91f3e72985abe
6027+
languageName: node
6028+
linkType: hard
6029+
60216030
"@types/eslint-scope@npm:^3.7.3":
60226031
version: 3.7.3
60236032
resolution: "@types/eslint-scope@npm:3.7.3"
@@ -6498,6 +6507,13 @@ __metadata:
64986507
languageName: node
64996508
linkType: hard
65006509

6510+
"@types/trusted-types@npm:*":
6511+
version: 2.0.3
6512+
resolution: "@types/trusted-types@npm:2.0.3"
6513+
checksum: 25eae736a8a6d24353c3e0108138935250f79d1d239f6fd6f3eb52d88476456ba946f8cb8f3130c6841d40534cafc2dd2326358d86966327c3c4a3d3eecaf585
6514+
languageName: node
6515+
linkType: hard
6516+
65016517
"@types/uglify-js@npm:*":
65026518
version: 3.13.1
65036519
resolution: "@types/uglify-js@npm:3.13.1"
@@ -19128,6 +19144,7 @@ __metadata:
1912819144
"@mui/icons-material": ^5.0.1
1912919145
"@mui/material": ^5.0.2
1913019146
"@testing-library/react": ^11.2.3
19147+
"@types/dompurify": ^3.0.2
1913119148
autosuggest-highlight: ^3.1.1
1913219149
clsx: ^1.1.1
1913319150
cross-env: ^5.2.0

0 commit comments

Comments
 (0)