@12-apps/ui 6.39.11 → 6.39.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/components/form/RichTextEditor/RichTextEditor.hooks.js +1 -1
- package/dist/_internal/components/form/RichTextEditor/RichTextEditor.js +2 -2
- package/dist/_internal/components/form/index.js +2 -2
- package/dist/_internal/components/index.js +2 -2
- package/dist/{chunk-774B27QA.js → chunk-EUACQR6N.js} +4 -3
- package/dist/chunk-EUACQR6N.js.map +1 -0
- package/dist/{chunk-7EM4THID.js → chunk-RTEY5COP.js} +2 -2
- package/dist/form/RichTextEditor.js +2 -2
- package/dist/types/components/form/RichTextEditor/RichTextEditor.hooks.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/form/RichTextEditor/RichTextEditor.hooks.ts +20 -3
- package/dist/chunk-774B27QA.js.map +0 -1
- /package/dist/{chunk-7EM4THID.js.map → chunk-RTEY5COP.js.map} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RichTextEditor,
|
|
3
3
|
RichTextEditor_default
|
|
4
|
-
} from "../../../../chunk-
|
|
5
|
-
import "../../../../chunk-
|
|
4
|
+
} from "../../../../chunk-RTEY5COP.js";
|
|
5
|
+
import "../../../../chunk-EUACQR6N.js";
|
|
6
6
|
import "../../../../chunk-UZOG6HNN.js";
|
|
7
7
|
import "../../../../chunk-33LAPBYB.js";
|
|
8
8
|
import "../../../../chunk-WVZBYTIK.js";
|
|
@@ -8,8 +8,8 @@ import "../../../chunk-A5UYPUS2.js";
|
|
|
8
8
|
import "../../../chunk-WMY276HD.js";
|
|
9
9
|
import {
|
|
10
10
|
RichTextEditor
|
|
11
|
-
} from "../../../chunk-
|
|
12
|
-
import "../../../chunk-
|
|
11
|
+
} from "../../../chunk-RTEY5COP.js";
|
|
12
|
+
import "../../../chunk-EUACQR6N.js";
|
|
13
13
|
import "../../../chunk-UZOG6HNN.js";
|
|
14
14
|
import "../../../chunk-33LAPBYB.js";
|
|
15
15
|
import "../../../chunk-WVZBYTIK.js";
|
|
@@ -196,8 +196,8 @@ import "../../chunk-YBVKLJAL.js";
|
|
|
196
196
|
import "../../chunk-WMY276HD.js";
|
|
197
197
|
import {
|
|
198
198
|
RichTextEditor
|
|
199
|
-
} from "../../chunk-
|
|
200
|
-
import "../../chunk-
|
|
199
|
+
} from "../../chunk-RTEY5COP.js";
|
|
200
|
+
import "../../chunk-EUACQR6N.js";
|
|
201
201
|
import "../../chunk-UZOG6HNN.js";
|
|
202
202
|
import "../../chunk-33LAPBYB.js";
|
|
203
203
|
import "../../chunk-WVZBYTIK.js";
|
|
@@ -34,6 +34,7 @@ var SANITIZE_CONFIG = {
|
|
|
34
34
|
ALLOW_DATA_ATTR: false,
|
|
35
35
|
KEEP_CONTENT: true
|
|
36
36
|
};
|
|
37
|
+
var sanitize = (html) => DOMPurify.sanitize(html, SANITIZE_CONFIG);
|
|
37
38
|
var textLengthOf = (html) => {
|
|
38
39
|
const probe = document.createElement("div");
|
|
39
40
|
probe.innerHTML = html;
|
|
@@ -50,7 +51,7 @@ var useEditorContent = ({
|
|
|
50
51
|
if (!editorRef.current) return;
|
|
51
52
|
const newContent = editorRef.current.innerHTML;
|
|
52
53
|
if (maxLength && (editorRef.current.textContent || "").length > maxLength) {
|
|
53
|
-
editorRef.current.innerHTML = content;
|
|
54
|
+
editorRef.current.innerHTML = sanitize(content);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
setContent(newContent);
|
|
@@ -106,7 +107,7 @@ var useRichTextEditor = ({
|
|
|
106
107
|
},
|
|
107
108
|
[disabled, readOnly, handleContentChange]
|
|
108
109
|
);
|
|
109
|
-
const sanitizedContent =
|
|
110
|
+
const sanitizedContent = sanitize(content);
|
|
110
111
|
useEffect(() => {
|
|
111
112
|
if (editorRef.current && editorRef.current.innerHTML !== sanitizedContent) {
|
|
112
113
|
editorRef.current.innerHTML = sanitizedContent;
|
|
@@ -126,4 +127,4 @@ var useRichTextEditor = ({
|
|
|
126
127
|
export {
|
|
127
128
|
useRichTextEditor
|
|
128
129
|
};
|
|
129
|
-
//# sourceMappingURL=chunk-
|
|
130
|
+
//# sourceMappingURL=chunk-EUACQR6N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/form/RichTextEditor/RichTextEditor.hooks.ts"],"sourcesContent":["import DOMPurify from 'dompurify';\nimport type React from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { applyFormatToRange } from './RichTextEditor.format';\n\n// Deliberately narrow: anything not listed is stripped, and no data-* attributes\n// survive, so pasted markup cannot smuggle in behaviour.\nconst SANITIZE_CONFIG = {\n ALLOWED_TAGS: [\n 'p', 'br', 'strong', 'em', 'u', 's', 'a', 'img', 'ul', 'ol', 'li',\n 'blockquote', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div',\n ],\n ALLOWED_ATTR: ['href', 'src', 'alt', 'target', 'rel', 'style', 'class'],\n ALLOW_DATA_ATTR: false,\n KEEP_CONTENT: true,\n};\n\n// The one way editor html reaches the DOM. Both writes (the render-path effect\n// and the over-limit restore) use it, so they cannot drift to different configs.\nconst sanitize = (html: string): string => DOMPurify.sanitize(html, SANITIZE_CONFIG);\n\n// INVARIANT: only ever pass SANITISED html here. Assigning innerHTML to a\n// detached element still fetches `<img src>` and fires its `onerror`, so raw\n// markup would execute. Its one caller passes `sanitizedContent`.\nconst textLengthOf = (html: string): number => {\n const probe = document.createElement('div');\n probe.innerHTML = html;\n return probe.textContent?.length || 0;\n};\n\n// The editable content and the limit that guards it.\nconst useEditorContent = ({\n editorRef,\n value,\n onChange,\n maxLength,\n}: {\n editorRef: React.RefObject<HTMLDivElement | null>;\n value?: string;\n onChange?: (value: string) => void;\n maxLength?: number;\n}) => {\n // `content` holds UNSANITISED html: the caller's raw `value` (read once, at\n // mount) until the first accepted input, and the editor's raw live innerHTML\n // after it. Every write of it to the DOM must go through `sanitize` below.\n // INVARIANT: if syncing a later `value` is ever added, the new value reaches\n // the DOM through the same sanitiser, never by a direct innerHTML write.\n const [content, setContent] = useState(value ?? '');\n\n const handleContentChange = useCallback(() => {\n if (!editorRef.current) return;\n\n const newContent = editorRef.current.innerHTML;\n\n // Over the limit, put the last accepted content back rather than truncating\n // mid-markup. `content` may be the caller's raw `value`, so the restore is\n // sanitised exactly as the render path is: writing it raw ran an `onerror`\n // handler carried in `value` (FUT-2685).\n if (maxLength && (editorRef.current.textContent || '').length > maxLength) {\n editorRef.current.innerHTML = sanitize(content);\n return;\n }\n\n // Emitting the live DOM is safe only because nothing but `sanitize()` ever\n // writes it: the editor renders empty and the effect below fills it. Give\n // the first render its own markup and this must emit `sanitize(newContent)`.\n setContent(newContent);\n onChange?.(newContent);\n }, [maxLength, onChange, content, editorRef]);\n\n return { content, handleContentChange };\n};\n\nconst useFocusState = ({ onFocus, onBlur }: { onFocus?: () => void; onBlur?: () => void }) => {\n const [isFocused, setIsFocused] = useState(false);\n\n const handleFocus = useCallback(() => {\n setIsFocused(true);\n onFocus?.();\n }, [onFocus]);\n\n const handleBlur = useCallback(() => {\n setIsFocused(false);\n onBlur?.();\n }, [onBlur]);\n\n return { isFocused, handleFocus, handleBlur };\n};\n\nexport const useRichTextEditor = ({\n value,\n onChange,\n onFocus,\n onBlur,\n disabled,\n readOnly,\n maxLength,\n}: {\n value?: string;\n onChange?: (value: string) => void;\n onFocus?: () => void;\n onBlur?: () => void;\n disabled?: boolean;\n readOnly?: boolean;\n maxLength?: number;\n}) => {\n const editorRef = useRef<HTMLDivElement>(null);\n const { content, handleContentChange } = useEditorContent({\n editorRef,\n value,\n onChange,\n maxLength,\n });\n const { isFocused, handleFocus, handleBlur } = useFocusState({ onFocus, onBlur });\n\n const applyFormat = useCallback(\n (formatType: string, formatValue?: string) => {\n if (disabled || readOnly || !editorRef.current) return;\n\n const selection = window.getSelection();\n // Nothing selected: give the editor focus so the next keystroke lands.\n if (!selection || selection.rangeCount === 0) {\n editorRef.current.focus();\n return;\n }\n\n applyFormatToRange({\n formatType,\n value: formatValue,\n range: selection.getRangeAt(0),\n editor: editorRef.current,\n });\n\n handleContentChange();\n editorRef.current.focus();\n },\n [disabled, readOnly, handleContentChange],\n );\n\n const sanitizedContent = sanitize(content);\n\n // Reflect the sanitized result back when it differs from what is on screen.\n useEffect(() => {\n if (editorRef.current && editorRef.current.innerHTML !== sanitizedContent) {\n editorRef.current.innerHTML = sanitizedContent;\n }\n }, [sanitizedContent]);\n\n return {\n editorRef,\n isFocused,\n characterCount: textLengthOf(sanitizedContent),\n handleContentChange,\n handleFocus,\n handleBlur,\n applyFormat,\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useRichTextEditor
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EUACQR6N.js";
|
|
4
4
|
import {
|
|
5
5
|
RichTextEditorToolbar
|
|
6
6
|
} from "./chunk-WVZBYTIK.js";
|
|
@@ -174,4 +174,4 @@ export {
|
|
|
174
174
|
RichTextEditor,
|
|
175
175
|
RichTextEditor_default
|
|
176
176
|
};
|
|
177
|
-
//# sourceMappingURL=chunk-
|
|
177
|
+
//# sourceMappingURL=chunk-RTEY5COP.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "../chunk-WMY276HD.js";
|
|
2
2
|
import {
|
|
3
3
|
RichTextEditor
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-RTEY5COP.js";
|
|
5
|
+
import "../chunk-EUACQR6N.js";
|
|
6
6
|
import "../chunk-UZOG6HNN.js";
|
|
7
7
|
import "../chunk-33LAPBYB.js";
|
|
8
8
|
import "../chunk-WVZBYTIK.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextEditor.hooks.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/RichTextEditor/RichTextEditor.hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"RichTextEditor.hooks.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/RichTextEditor/RichTextEditor.hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAyF/B,eAAO,MAAM,iBAAiB,GAAI,sEAQ/B;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;;;;;8BAWgB,MAAM,gBAAgB,MAAM;CAyC5C,CAAC"}
|
package/package.json
CHANGED
|
@@ -16,6 +16,13 @@ const SANITIZE_CONFIG = {
|
|
|
16
16
|
KEEP_CONTENT: true,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// The one way editor html reaches the DOM. Both writes (the render-path effect
|
|
20
|
+
// and the over-limit restore) use it, so they cannot drift to different configs.
|
|
21
|
+
const sanitize = (html: string): string => DOMPurify.sanitize(html, SANITIZE_CONFIG);
|
|
22
|
+
|
|
23
|
+
// INVARIANT: only ever pass SANITISED html here. Assigning innerHTML to a
|
|
24
|
+
// detached element still fetches `<img src>` and fires its `onerror`, so raw
|
|
25
|
+
// markup would execute. Its one caller passes `sanitizedContent`.
|
|
19
26
|
const textLengthOf = (html: string): number => {
|
|
20
27
|
const probe = document.createElement('div');
|
|
21
28
|
probe.innerHTML = html;
|
|
@@ -34,6 +41,11 @@ const useEditorContent = ({
|
|
|
34
41
|
onChange?: (value: string) => void;
|
|
35
42
|
maxLength?: number;
|
|
36
43
|
}) => {
|
|
44
|
+
// `content` holds UNSANITISED html: the caller's raw `value` (read once, at
|
|
45
|
+
// mount) until the first accepted input, and the editor's raw live innerHTML
|
|
46
|
+
// after it. Every write of it to the DOM must go through `sanitize` below.
|
|
47
|
+
// INVARIANT: if syncing a later `value` is ever added, the new value reaches
|
|
48
|
+
// the DOM through the same sanitiser, never by a direct innerHTML write.
|
|
37
49
|
const [content, setContent] = useState(value ?? '');
|
|
38
50
|
|
|
39
51
|
const handleContentChange = useCallback(() => {
|
|
@@ -42,12 +54,17 @@ const useEditorContent = ({
|
|
|
42
54
|
const newContent = editorRef.current.innerHTML;
|
|
43
55
|
|
|
44
56
|
// Over the limit, put the last accepted content back rather than truncating
|
|
45
|
-
// mid-markup.
|
|
57
|
+
// mid-markup. `content` may be the caller's raw `value`, so the restore is
|
|
58
|
+
// sanitised exactly as the render path is: writing it raw ran an `onerror`
|
|
59
|
+
// handler carried in `value` (FUT-2685).
|
|
46
60
|
if (maxLength && (editorRef.current.textContent || '').length > maxLength) {
|
|
47
|
-
editorRef.current.innerHTML = content;
|
|
61
|
+
editorRef.current.innerHTML = sanitize(content);
|
|
48
62
|
return;
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
// Emitting the live DOM is safe only because nothing but `sanitize()` ever
|
|
66
|
+
// writes it: the editor renders empty and the effect below fills it. Give
|
|
67
|
+
// the first render its own markup and this must emit `sanitize(newContent)`.
|
|
51
68
|
setContent(newContent);
|
|
52
69
|
onChange?.(newContent);
|
|
53
70
|
}, [maxLength, onChange, content, editorRef]);
|
|
@@ -121,7 +138,7 @@ export const useRichTextEditor = ({
|
|
|
121
138
|
[disabled, readOnly, handleContentChange],
|
|
122
139
|
);
|
|
123
140
|
|
|
124
|
-
const sanitizedContent =
|
|
141
|
+
const sanitizedContent = sanitize(content);
|
|
125
142
|
|
|
126
143
|
// Reflect the sanitized result back when it differs from what is on screen.
|
|
127
144
|
useEffect(() => {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/form/RichTextEditor/RichTextEditor.hooks.ts"],"sourcesContent":["import DOMPurify from 'dompurify';\nimport type React from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { applyFormatToRange } from './RichTextEditor.format';\n\n// Deliberately narrow: anything not listed is stripped, and no data-* attributes\n// survive, so pasted markup cannot smuggle in behaviour.\nconst SANITIZE_CONFIG = {\n ALLOWED_TAGS: [\n 'p', 'br', 'strong', 'em', 'u', 's', 'a', 'img', 'ul', 'ol', 'li',\n 'blockquote', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div',\n ],\n ALLOWED_ATTR: ['href', 'src', 'alt', 'target', 'rel', 'style', 'class'],\n ALLOW_DATA_ATTR: false,\n KEEP_CONTENT: true,\n};\n\nconst textLengthOf = (html: string): number => {\n const probe = document.createElement('div');\n probe.innerHTML = html;\n return probe.textContent?.length || 0;\n};\n\n// The editable content and the limit that guards it.\nconst useEditorContent = ({\n editorRef,\n value,\n onChange,\n maxLength,\n}: {\n editorRef: React.RefObject<HTMLDivElement | null>;\n value?: string;\n onChange?: (value: string) => void;\n maxLength?: number;\n}) => {\n const [content, setContent] = useState(value ?? '');\n\n const handleContentChange = useCallback(() => {\n if (!editorRef.current) return;\n\n const newContent = editorRef.current.innerHTML;\n\n // Over the limit, put the last accepted content back rather than truncating\n // mid-markup.\n if (maxLength && (editorRef.current.textContent || '').length > maxLength) {\n editorRef.current.innerHTML = content;\n return;\n }\n\n setContent(newContent);\n onChange?.(newContent);\n }, [maxLength, onChange, content, editorRef]);\n\n return { content, handleContentChange };\n};\n\nconst useFocusState = ({ onFocus, onBlur }: { onFocus?: () => void; onBlur?: () => void }) => {\n const [isFocused, setIsFocused] = useState(false);\n\n const handleFocus = useCallback(() => {\n setIsFocused(true);\n onFocus?.();\n }, [onFocus]);\n\n const handleBlur = useCallback(() => {\n setIsFocused(false);\n onBlur?.();\n }, [onBlur]);\n\n return { isFocused, handleFocus, handleBlur };\n};\n\nexport const useRichTextEditor = ({\n value,\n onChange,\n onFocus,\n onBlur,\n disabled,\n readOnly,\n maxLength,\n}: {\n value?: string;\n onChange?: (value: string) => void;\n onFocus?: () => void;\n onBlur?: () => void;\n disabled?: boolean;\n readOnly?: boolean;\n maxLength?: number;\n}) => {\n const editorRef = useRef<HTMLDivElement>(null);\n const { content, handleContentChange } = useEditorContent({\n editorRef,\n value,\n onChange,\n maxLength,\n });\n const { isFocused, handleFocus, handleBlur } = useFocusState({ onFocus, onBlur });\n\n const applyFormat = useCallback(\n (formatType: string, formatValue?: string) => {\n if (disabled || readOnly || !editorRef.current) return;\n\n const selection = window.getSelection();\n // Nothing selected: give the editor focus so the next keystroke lands.\n if (!selection || selection.rangeCount === 0) {\n editorRef.current.focus();\n return;\n }\n\n applyFormatToRange({\n formatType,\n value: formatValue,\n range: selection.getRangeAt(0),\n editor: editorRef.current,\n });\n\n handleContentChange();\n editorRef.current.focus();\n },\n [disabled, readOnly, handleContentChange],\n );\n\n const sanitizedContent = DOMPurify.sanitize(content, SANITIZE_CONFIG);\n\n // Reflect the sanitized result back when it differs from what is on screen.\n useEffect(() => {\n if (editorRef.current && editorRef.current.innerHTML !== sanitizedContent) {\n editorRef.current.innerHTML = sanitizedContent;\n }\n }, [sanitizedContent]);\n\n return {\n editorRef,\n isFocused,\n characterCount: textLengthOf(sanitizedContent),\n handleContentChange,\n handleFocus,\n handleBlur,\n applyFormat,\n };\n};\n"],"mappings":"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","names":[]}
|
|
File without changes
|