@wistia/rich-text-editor 0.0.0-beta.1f9acbaf.d44aebf

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/index.js ADDED
@@ -0,0 +1,1232 @@
1
+
2
+ /*
3
+ * @license @wistia/rich-text-editor v0.0.0-beta.1f9acbaf.d44aebf
4
+ *
5
+ * Copyright (c) 2021-2026, Wistia, Inc. and its affiliates.
6
+ *
7
+ * This source code is unlicensed, all rights reserved.
8
+ */
9
+
10
+ import { isArray, isNil, isNonEmptyString, isNotNil, isNotUndefined, isString, isUndefined } from "@wistia/type-guards";
11
+ import { isHotkey } from "is-hotkey";
12
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
13
+ import { Editor, Element, Node, Path, Range, Text, Transforms, createEditor } from "slate";
14
+ import { withHistory } from "slate-history";
15
+ import { Editable, ReactEditor, Slate, useFocused, useSelected, useSlate, withReact } from "slate-react";
16
+ import { css, styled } from "styled-components";
17
+ import { jsx, jsxs } from "react/jsx-runtime";
18
+ import { jsx as jsx$1 } from "slate-hyperscript";
19
+ import isEqual from "react-fast-compare";
20
+ //#region \0rolldown/runtime.js
21
+ var __defProp = Object.defineProperty;
22
+ var __exportAll = (all, no_symbols) => {
23
+ let target = {};
24
+ for (var name in all) __defProp(target, name, {
25
+ get: all[name],
26
+ enumerable: true
27
+ });
28
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
29
+ return target;
30
+ };
31
+ //#endregion
32
+ //#region src/components/Icon/iconMap.ts
33
+ const iconMap = {
34
+ "block-quote": "BlockQuote",
35
+ bold: "Bold",
36
+ "bulleted-list": "BulletedList",
37
+ heading: "Heading",
38
+ "heading-two": "HeadingTwo",
39
+ image: "Image",
40
+ italic: "Italic",
41
+ link: "Link",
42
+ "numbered-list": "NumberedList",
43
+ strikethrough: "Strikethrough",
44
+ underline: "Underline"
45
+ };
46
+ //#endregion
47
+ //#region src/components/Icon/icons/BlockQuote.tsx
48
+ const BlockQuote = (props) => /* @__PURE__ */ jsx("svg", {
49
+ ...props,
50
+ children: /* @__PURE__ */ jsx("path", {
51
+ d: "M4.2125 11.0375C4.35417 11.1792 4.53333 11.25 4.75 11.25H19.25C19.4667 11.25 19.6458 11.1792 19.7875 11.0375C19.9292 10.8958 20 10.7167 20 10.5C20 10.2833 19.9292 10.1042 19.7875 9.9625C19.6458 9.82083 19.4667 9.75 19.25 9.75H4.75C4.53333 9.75 4.35417 9.82083 4.2125 9.9625C4.07083 10.1042 4 10.2833 4 10.5C4 10.7167 4.07083 10.8958 4.2125 11.0375ZM4.2125 14.0375C4.35417 14.1792 4.53333 14.25 4.75 14.25H12.975C13.1917 14.25 13.3708 14.1792 13.5125 14.0375C13.6542 13.8958 13.725 13.7167 13.725 13.5C13.725 13.2833 13.6542 13.1042 13.5125 12.9625C13.3708 12.8208 13.1917 12.75 12.975 12.75H4.75C4.53333 12.75 4.35417 12.8208 4.2125 12.9625C4.07083 13.1042 4 13.2833 4 13.5C4 13.7167 4.07083 13.8958 4.2125 14.0375ZM1.5 10C1.22386 10 1 10.2239 1 10.5V14C1 14.2761 1.22386 14.5 1.5 14.5C1.77614 14.5 2 14.2761 2 14V10.5C2 10.2239 1.77614 10 1.5 10Z",
52
+ fill: "currentColor"
53
+ })
54
+ });
55
+ //#endregion
56
+ //#region src/components/Icon/icons/Bold.tsx
57
+ const Bold = (props) => /* @__PURE__ */ jsx("svg", {
58
+ ...props,
59
+ children: /* @__PURE__ */ jsx("path", {
60
+ d: "M16.9995 11.7C18.354 10.743 19.302 9.17101 19.302 7.71451C19.302 4.48501 16.86 1.99951 13.722 1.99951H4.998V22.0005H14.823C17.7375 22.0005 19.998 19.5705 19.998 16.5855C19.998 14.415 18.798 12.5565 16.998 11.7H16.9995ZM9.186 5.57101H13.371C14.5305 5.57101 15.465 6.52801 15.465 7.71451C15.465 8.89951 14.5305 9.85651 13.3725 9.85651H9.186V5.57101ZM14.07 18.429H9.186V14.1435H14.07C15.228 14.1435 16.1625 15.1005 16.1625 16.2855C16.1625 17.4705 15.228 18.429 14.07 18.429V18.429Z",
61
+ fill: "currentColor"
62
+ })
63
+ });
64
+ //#endregion
65
+ //#region src/components/Icon/icons/BulletedList.tsx
66
+ const BulletedList = (props) => /* @__PURE__ */ jsx("svg", {
67
+ ...props,
68
+ children: /* @__PURE__ */ jsx("path", {
69
+ d: "M4.0005 10.5C3.1695 10.5 2.5005 11.1705 2.5005 12C2.5005 12.8295 3.1695 13.5 4.0005 13.5C4.83 13.5 5.5005 12.8295 5.5005 12C5.5005 11.1705 4.83 10.5 4.0005 10.5ZM4.0005 4.5C3.1695 4.5 2.5005 5.1705 2.5005 6C2.5005 6.8295 3.1695 7.5 4.0005 7.5C4.83 7.5 5.5005 6.8295 5.5005 6C5.5005 5.1705 4.83 4.5 4.0005 4.5ZM4.0005 16.5C3.1695 16.5 2.5005 17.1795 2.5005 18C2.5005 18.8205 3.18 19.5 4.0005 19.5C4.8195 19.5 5.5005 18.8205 5.5005 18C5.5005 17.1795 4.83 16.5 4.0005 16.5ZM7.0005 19.0005H21V16.9995H7.0005V19.0005ZM7.0005 13.0005H21V10.9995H7.0005V13.0005ZM7.0005 4.9995V7.0005H21V4.9995H7.0005Z",
70
+ fill: "currentColor"
71
+ })
72
+ });
73
+ //#endregion
74
+ //#region src/components/Icon/icons/Heading.tsx
75
+ const Heading = (props) => /* @__PURE__ */ jsx("svg", {
76
+ ...props,
77
+ children: /* @__PURE__ */ jsx("path", {
78
+ d: "M7.9995 3V6.7995H14.154V22.0005H17.8455V6.7995H24V3H7.9995ZM0 12.9H3.666V22.0005H7.335V12.9H11.001V9H0V12.9Z",
79
+ fill: "currentColor"
80
+ })
81
+ });
82
+ //#endregion
83
+ //#region src/components/Icon/icons/HeadingTwo.tsx
84
+ const HeadingTwo = (props) => /* @__PURE__ */ jsx("svg", {
85
+ ...props,
86
+ children: /* @__PURE__ */ jsx("path", {
87
+ d: "M3 17V7H4.5V11.25H9V7H10.5V17H9V12.75H4.5V17H3ZM13.5 17V12.75C13.5 12.3333 13.6458 11.9792 13.9375 11.6875C14.2292 11.3958 14.5833 11.25 15 11.25H19.5V8.5H13.5V7H19.5C19.9167 7 20.2708 7.14583 20.5625 7.4375C20.8542 7.72917 21 8.08333 21 8.5V11.25C21 11.6667 20.8542 12.0208 20.5625 12.3125C20.2708 12.6042 19.9167 12.75 19.5 12.75H15V15.5H21V17H13.5Z",
88
+ fill: "currentColor"
89
+ })
90
+ });
91
+ //#endregion
92
+ //#region src/components/Icon/icons/Image.tsx
93
+ const Image$1 = (props) => /* @__PURE__ */ jsx("svg", {
94
+ ...props,
95
+ children: /* @__PURE__ */ jsx("path", {
96
+ d: "M21 19.0005V4.9995C21 3.9 20.1 3 19.0005 3H4.9995C3.9 3 3 3.9 3 4.9995V19.0005C3 20.1 3.9 21 4.9995 21H19.0005C20.1 21 21 20.1 21 19.0005ZM8.5005 13.5L10.9995 16.5105L14.5005 12L19.0005 18H4.9995L8.5005 13.5Z",
97
+ fill: "currentColor"
98
+ })
99
+ });
100
+ //#endregion
101
+ //#region src/components/Icon/icons/Italic.tsx
102
+ const Italic = (props) => /* @__PURE__ */ jsx("svg", {
103
+ ...props,
104
+ children: /* @__PURE__ */ jsx("path", {
105
+ d: "M8.8335 1.99951V6.28501H12.0555L7.0695 17.715H3V22.0005H14.667V17.715H11.4435L16.431 6.28501H20.5005V1.99951H8.8335Z",
106
+ fill: "currentColor"
107
+ })
108
+ });
109
+ //#endregion
110
+ //#region src/components/Icon/icons/Link.tsx
111
+ const Link = (props) => /* @__PURE__ */ jsx("svg", {
112
+ ...props,
113
+ children: /* @__PURE__ */ jsx("path", {
114
+ d: "M2.28 12C2.28 9.948 3.948 8.28 6 8.28H10.8V6H6C2.688 6 0 8.688 0 12C0 15.312 2.688 18 6 18H10.8V15.72H6C3.948 15.72 2.28 14.052 2.28 12ZM7.2 13.2H16.8V10.8H7.2V13.2ZM18 6H13.2V8.28H18C20.052 8.28 21.72 9.948 21.72 12C21.72 14.052 20.052 15.72 18 15.72H13.2V18H18C21.312 18 24 15.312 24 12C24 8.688 21.312 6 18 6Z",
115
+ fill: "currentColor"
116
+ })
117
+ });
118
+ //#endregion
119
+ //#region src/components/Icon/icons/NumberedList.tsx
120
+ const NumberedList = (props) => /* @__PURE__ */ jsx("svg", {
121
+ ...props,
122
+ children: /* @__PURE__ */ jsx("path", {
123
+ d: "M3.425 20C3.30833 20 3.20833 19.9583 3.125 19.875C3.04167 19.7917 3 19.6917 3 19.575C3 19.4583 3.04167 19.3583 3.125 19.275C3.20833 19.1917 3.30833 19.15 3.425 19.15H5.1V18.5H4.475C4.35833 18.5 4.25833 18.4583 4.175 18.375C4.09167 18.2917 4.05 18.1917 4.05 18.075C4.05 17.9583 4.09167 17.8583 4.175 17.775C4.25833 17.6917 4.35833 17.65 4.475 17.65H5.1V17H3.425C3.30833 17 3.20833 16.9583 3.125 16.875C3.04167 16.7917 3 16.6917 3 16.575C3 16.4583 3.04167 16.3583 3.125 16.275C3.20833 16.1917 3.30833 16.15 3.425 16.15H5.525C5.64167 16.15 5.74167 16.1917 5.825 16.275C5.90833 16.3583 5.95 16.4583 5.95 16.575V19.575C5.95 19.6917 5.90833 19.7917 5.825 19.875C5.74167 19.9583 5.64167 20 5.525 20H3.425ZM4.475 7.9C4.35833 7.9 4.25833 7.85833 4.175 7.775C4.09167 7.69167 4.05 7.59167 4.05 7.475V4.85H3.425C3.30833 4.85 3.20833 4.80833 3.125 4.725C3.04167 4.64167 3 4.54167 3 4.425C3 4.30833 3.04167 4.20833 3.125 4.125C3.20833 4.04167 3.30833 4 3.425 4H4.475C4.59167 4 4.69167 4.04167 4.775 4.125C4.85833 4.20833 4.9 4.30833 4.9 4.425V7.475C4.9 7.59167 4.85833 7.69167 4.775 7.775C4.69167 7.85833 4.59167 7.9 4.475 7.9V7.9ZM3.425 13.925C3.30833 13.925 3.20833 13.8833 3.125 13.8C3.04167 13.7167 3 13.6167 3 13.5V13.275C3 13.2583 3.03333 13.1667 3.1 13L4.875 10.925H3.425C3.30833 10.925 3.20833 10.8833 3.125 10.8C3.04167 10.7167 3 10.6167 3 10.5C3 10.3833 3.04167 10.2833 3.125 10.2C3.20833 10.1167 3.30833 10.075 3.425 10.075H5.525C5.64167 10.075 5.74167 10.1167 5.825 10.2C5.90833 10.2833 5.95 10.3833 5.95 10.5V10.725C5.95 10.7417 5.91667 10.8333 5.85 11L4.05 13.075H5.525C5.64167 13.075 5.74167 13.1167 5.825 13.2C5.90833 13.2833 5.95 13.3833 5.95 13.5C5.95 13.6167 5.90833 13.7167 5.825 13.8C5.74167 13.8833 5.64167 13.925 5.525 13.925H3.425ZM8.975 18.775C8.75833 18.775 8.57917 18.7042 8.4375 18.5625C8.29583 18.4208 8.225 18.2417 8.225 18.025C8.225 17.8083 8.29583 17.6292 8.4375 17.4875C8.57917 17.3458 8.75833 17.275 8.975 17.275H20.25C20.4667 17.275 20.6458 17.3458 20.7875 17.4875C20.9292 17.6292 21 17.8083 21 18.025C21 18.2417 20.9292 18.4208 20.7875 18.5625C20.6458 18.7042 20.4667 18.775 20.25 18.775H8.975ZM8.975 12.7C8.75833 12.7 8.57917 12.6292 8.4375 12.4875C8.29583 12.3458 8.225 12.1667 8.225 11.95C8.225 11.7333 8.29583 11.5542 8.4375 11.4125C8.57917 11.2708 8.75833 11.2 8.975 11.2H20.25C20.4667 11.2 20.6458 11.2708 20.7875 11.4125C20.9292 11.5542 21 11.7333 21 11.95C21 12.1667 20.9292 12.3458 20.7875 12.4875C20.6458 12.6292 20.4667 12.7 20.25 12.7H8.975ZM8.975 6.625C8.75833 6.625 8.57917 6.55417 8.4375 6.4125C8.29583 6.27083 8.225 6.09167 8.225 5.875C8.225 5.65833 8.29583 5.47917 8.4375 5.3375C8.57917 5.19583 8.75833 5.125 8.975 5.125H20.25C20.4667 5.125 20.6458 5.19583 20.7875 5.3375C20.9292 5.47917 21 5.65833 21 5.875C21 6.09167 20.9292 6.27083 20.7875 6.4125C20.6458 6.55417 20.4667 6.625 20.25 6.625H8.975Z",
124
+ fill: "currentColor"
125
+ })
126
+ });
127
+ //#endregion
128
+ //#region src/components/Icon/icons/Strikethrough.tsx
129
+ const Strikethrough = (props) => /* @__PURE__ */ jsx("svg", {
130
+ ...props,
131
+ children: /* @__PURE__ */ jsx("path", {
132
+ d: "M2 13.75V12.25H22V13.75H2ZM10.75 10.75V6.5H5V4H19V6.5H13.25V10.75H10.75ZM10.75 20V15.25H13.25V20H10.75Z",
133
+ fill: "currentColor"
134
+ })
135
+ });
136
+ //#endregion
137
+ //#region src/components/Icon/icons/Underline.tsx
138
+ const Underline = (props) => /* @__PURE__ */ jsx("svg", {
139
+ ...props,
140
+ children: /* @__PURE__ */ jsx("path", {
141
+ d: "M5 21V19.5H19V21H5ZM12 17C10.3167 17 8.89583 16.425 7.7375 15.275C6.57917 14.125 6 12.7167 6 11.05V3H8V11.1C8 12.2 8.38333 13.125 9.15 13.875C9.91667 14.625 10.8667 15 12 15C13.1333 15 14.0833 14.625 14.85 13.875C15.6167 13.125 16 12.2 16 11.1V3H18V11.05C18 12.7167 17.4208 14.125 16.2625 15.275C15.1042 16.425 13.6833 17 12 17Z",
142
+ fill: "currentColor"
143
+ })
144
+ });
145
+ //#endregion
146
+ //#region src/components/Icon/icons/index.ts
147
+ var icons_exports = /* @__PURE__ */ __exportAll({
148
+ BlockQuote: () => BlockQuote,
149
+ Bold: () => Bold,
150
+ BulletedList: () => BulletedList,
151
+ Heading: () => Heading,
152
+ HeadingTwo: () => HeadingTwo,
153
+ Image: () => Image$1,
154
+ Italic: () => Italic,
155
+ Link: () => Link,
156
+ NumberedList: () => NumberedList,
157
+ Strikethrough: () => Strikethrough,
158
+ Underline: () => Underline
159
+ });
160
+ //#endregion
161
+ //#region src/components/Icon/Icon.tsx
162
+ const iconSizeMap = {
163
+ sm: "12",
164
+ md: "16",
165
+ lg: "24",
166
+ xl: "32",
167
+ xxl: "48"
168
+ };
169
+ const Icon = ({ color = "currentColor", size = "lg", style, type, ...otherProps }) => {
170
+ if (isNil(type)) throw new Error("An Icon component requires a `type` prop to be provided");
171
+ if (isNil(iconMap[type])) throw new Error(`Type "${type}" does not exist, please update type prop in Icon component.`);
172
+ if (isNil(iconSizeMap[size])) throw new Error(`Size "${size}" does not exist, please update size prop in Icon component.`);
173
+ const IconElement = icons_exports[iconMap[type]];
174
+ const defaultStyle = {
175
+ verticalAlign: "middle",
176
+ ...style
177
+ };
178
+ return /* @__PURE__ */ jsx(IconElement, {
179
+ "aria-hidden": "true",
180
+ color,
181
+ height: `${iconSizeMap[size]}px`,
182
+ role: "presentation",
183
+ style: defaultStyle,
184
+ viewBox: "0 0 24 24",
185
+ width: `${iconSizeMap[size]}px`,
186
+ xmlns: "http://www.w3.org/2000/svg",
187
+ ...otherProps
188
+ });
189
+ };
190
+ Icon.displayName = "Icon";
191
+ //#endregion
192
+ //#region src/components/RichTextEditor/ToolbarButton.tsx
193
+ const StyledToolbarButton = styled.button`
194
+ --rte-toolbar-button-color: ${({ $isActive }) => $isActive ? "var(--rte-accent-color, #2949e5)" : "var(--rte-icon-color, #62626c)"};
195
+
196
+ align-items: center;
197
+ appearance: none;
198
+ background-color: transparent;
199
+ background-image: none;
200
+ border: none;
201
+ border-radius: var(--rte-toolbar-button-radius, 1000px);
202
+ box-shadow: none;
203
+ color: var(--rte-toolbar-button-color);
204
+ cursor: pointer;
205
+ display: inline-flex;
206
+ font: inherit;
207
+ height: 24px;
208
+ justify-content: center;
209
+ line-height: 1;
210
+ padding: 0;
211
+ transition: all 110ms;
212
+ vertical-align: middle;
213
+ width: 24px;
214
+
215
+ svg {
216
+ height: 16px;
217
+ width: 16px;
218
+ }
219
+
220
+ &::-moz-focus-inner {
221
+ border: 0;
222
+ padding: 0;
223
+ }
224
+
225
+ &:focus-visible {
226
+ outline: thin dotted #999999;
227
+ }
228
+
229
+ &:hover,
230
+ &:focus-visible {
231
+ background-color: color-mix(in srgb, var(--rte-toolbar-button-color) 20%, transparent);
232
+ }
233
+
234
+ &:active {
235
+ background-color: color-mix(in srgb, var(--rte-toolbar-button-color) 40%, transparent);
236
+ }
237
+
238
+ &:disabled {
239
+ background-color: transparent;
240
+ color: color-mix(in srgb, var(--rte-toolbar-button-color) 50%, transparent);
241
+ cursor: not-allowed;
242
+ }
243
+ `;
244
+ const ToolbarButton = ({ icon, isActive, label, ...props }) => /* @__PURE__ */ jsx(StyledToolbarButton, {
245
+ ...props,
246
+ $isActive: isActive,
247
+ "aria-label": label,
248
+ type: "button",
249
+ children: icon
250
+ });
251
+ //#endregion
252
+ //#region src/helpers/isUrl/isUrl.ts
253
+ /**
254
+ * Loosely validates a URL string
255
+ *
256
+ * @param {*} str - a string to validate - could be an unknown type
257
+ * @return {boolean} - whether string appears to be a url
258
+ */
259
+ const isUrl = (str) => {
260
+ const protocolAndDomain = /^(?:\w+:)?\/\/(\S+)$/;
261
+ const localhostDomain = /^localhost[:?\d]*(?:[^:?\d]\S*)?$/;
262
+ const nonLocalhostDomain = /^[^\s.]+\.\S{2,}$/;
263
+ if (!isString(str)) return false;
264
+ const match = protocolAndDomain.exec(str);
265
+ if (!match) return false;
266
+ const remaining = match[1];
267
+ if (remaining === void 0 || !remaining) return false;
268
+ return localhostDomain.test(remaining) || nonLocalhostDomain.test(remaining);
269
+ };
270
+ //#endregion
271
+ //#region src/components/RichTextEditor/utilities/withImagesPlugin.ts
272
+ const emptyTextNode$1 = { text: "" };
273
+ const imageExtensions = [
274
+ "jpg",
275
+ "jpeg",
276
+ "png",
277
+ "gif",
278
+ "webp",
279
+ "avif",
280
+ "svg",
281
+ "tiff",
282
+ "bmp",
283
+ "dcm"
284
+ ];
285
+ const isImageUrl = (url) => {
286
+ if (!isUrl(url)) return false;
287
+ const hasImageExtension = imageExtensions.find((event) => url.includes(event));
288
+ return isNotUndefined(hasImageExtension) && Boolean(hasImageExtension);
289
+ };
290
+ const insertImage = (editor, url) => {
291
+ if (!isImageUrl(url)) return false;
292
+ const image = {
293
+ type: "image",
294
+ url,
295
+ children: [emptyTextNode$1]
296
+ };
297
+ Transforms.insertNodes(editor, image);
298
+ return true;
299
+ };
300
+ const withImages = (editor) => {
301
+ const { normalizeNode, isVoid } = editor;
302
+ editor.isVoid = (element) => element.type === "image" ? true : isVoid(element);
303
+ editor.normalizeNode = ([node, path]) => {
304
+ if (Element.isElement(node) && node.type === "image" && node.url !== void 0) {
305
+ const paragraph = {
306
+ type: "paragraph",
307
+ children: [emptyTextNode$1]
308
+ };
309
+ const pathIndex = isNotUndefined(path[0]) ? path[0] + 1 : 1;
310
+ Transforms.insertNodes(editor, paragraph, { at: [pathIndex] });
311
+ Transforms.move(editor);
312
+ }
313
+ if (Element.isElement(node) && node.type === "image" && node.url === void 0 && isNotUndefined(node.children) && isNotUndefined(node.children[0]) && isNotUndefined(node.children[0].text) && node.children[0].text.trim() !== "") Transforms.setNodes(editor, { type: "span" }, {
314
+ match: (matchNode) => Element.isElement(matchNode) && matchNode.type === "image" && matchNode.url === void 0 && isNotUndefined(matchNode.children) && Text.isText(matchNode.children[0]) && isNotNil(matchNode.children[0].text) && matchNode.children[0].text.trim() !== "",
315
+ split: true,
316
+ at: path,
317
+ mode: "highest"
318
+ });
319
+ normalizeNode([node, path]);
320
+ };
321
+ return editor;
322
+ };
323
+ //#endregion
324
+ //#region src/components/RichTextEditor/utilities/withLinksPlugin.ts
325
+ const isHashUrl = (urlString) => {
326
+ return /^#.+$/i.test(urlString);
327
+ };
328
+ const isLinkActive = (editor) => {
329
+ const [link] = Editor.nodes(editor, { match: (node) => Element.isElement(node) && node.type === "link" });
330
+ return Boolean(link);
331
+ };
332
+ const unwrapLink = (editor) => {
333
+ Transforms.unwrapNodes(editor, { match: (node) => Element.isElement(node) && node.type === "link" });
334
+ };
335
+ const wrapLink = (editor, url) => {
336
+ if (isLinkActive(editor)) unwrapLink(editor);
337
+ const { selection } = editor;
338
+ const isCollapsed = selection && Range.isCollapsed(selection);
339
+ const link = {
340
+ type: "link",
341
+ url,
342
+ children: isCollapsed === true ? [{ text: url }] : []
343
+ };
344
+ if (isCollapsed === true) Transforms.insertNodes(editor, link);
345
+ else {
346
+ Transforms.wrapNodes(editor, link, { split: true });
347
+ Transforms.collapse(editor, { edge: "end" });
348
+ }
349
+ };
350
+ const withLinks = (editor) => {
351
+ const { insertData, insertText, isInline, normalizeNode } = editor;
352
+ editor.isInline = (element) => element.type === "link" ? true : isInline(element);
353
+ editor.insertText = (text) => {
354
+ if (text && isUrl(text)) wrapLink(editor, text);
355
+ else insertText(text);
356
+ };
357
+ editor.insertData = (data) => {
358
+ const text = data.getData("text/plain");
359
+ if (text && isUrl(text)) wrapLink(editor, text);
360
+ else insertData(data);
361
+ };
362
+ editor.normalizeNode = ([node, path]) => {
363
+ if (Element.isElement(node) && node.type === "link" && Node.string(node) === "") {
364
+ Transforms.unwrapNodes(editor, { at: path });
365
+ Transforms.select(editor, Path.next(path));
366
+ }
367
+ normalizeNode([node, path]);
368
+ };
369
+ return editor;
370
+ };
371
+ const insertLink = (editor) => {
372
+ if (isLinkActive(editor)) unwrapLink(editor);
373
+ else {
374
+ const url = prompt("Enter the URL of the link:");
375
+ if (!isNonEmptyString(url)) return;
376
+ if (url.includes(" ")) {
377
+ alert("The URL provided had spaces. Try again without spaces.");
378
+ return;
379
+ }
380
+ if (!isUrl(url) && !isHashUrl(url)) {
381
+ alert("The URL provided looks invalid. Try again. Make sure to include the protocol (ex. https://) in the URL.");
382
+ return;
383
+ }
384
+ if (editor.selection) wrapLink(editor, url);
385
+ }
386
+ };
387
+ //#endregion
388
+ //#region src/components/RichTextEditor/utilities/handleListEscape.ts
389
+ const OverriddenEditor = {
390
+ ...Editor,
391
+ isBlock: Editor.isBlock
392
+ };
393
+ const newLine = {
394
+ type: "paragraph",
395
+ children: [{
396
+ text: "",
397
+ marks: []
398
+ }]
399
+ };
400
+ const isLastChildBlankText = (listChildren) => {
401
+ if (listChildren.length === 1) return (listChildren.at(-1)?.text)?.trim() === "";
402
+ return listChildren.find((el) => el.type === "link")?.children[0]?.text?.trim() === "";
403
+ };
404
+ const handleListEscape = (editor) => {
405
+ const { selection } = editor;
406
+ const textIsNotSelected = selection && Range.isCollapsed(selection);
407
+ const selectedElement = editor.children[selection.anchor.path[0]];
408
+ const isList = selectedElement.type === "bulleted-list" || selectedElement.type === "numbered-list";
409
+ const blockAbove = OverriddenEditor.above(editor, { match: (node) => Element.isElement(node) && OverriddenEditor.isBlock(editor, node) });
410
+ if ((textIsNotSelected ?? false) && isList && isLastChildBlankText(blockAbove[0].children)) {
411
+ Transforms.removeNodes(editor, { hanging: true });
412
+ Transforms.insertNodes(editor, newLine);
413
+ Transforms.unwrapNodes(editor, {
414
+ match: ({ type }) => type === "bulleted-list" || type === "numbered-list",
415
+ all: true,
416
+ split: true
417
+ });
418
+ }
419
+ };
420
+ //#endregion
421
+ //#region src/components/RichTextEditor/utilities/rteFormatTypes.ts
422
+ const rteFormatTypes = Object.freeze({
423
+ BOLD: "BOLD",
424
+ ITALIC: "ITALIC",
425
+ UNDERLINE: "UNDERLINE",
426
+ STRIKETHROUGH: "STRIKETHROUGH",
427
+ HEADING_1: "HEADING_1",
428
+ HEADING_2: "HEADING_2",
429
+ BLOCKQUOTE: "BLOCKQUOTE",
430
+ NUMBERED_LIST: "NUMBERED_LIST",
431
+ BULLETED_LIST: "BULLETED_LIST",
432
+ LINK: "LINK",
433
+ IMAGE: "IMAGE"
434
+ });
435
+ //#endregion
436
+ //#region src/components/RichTextEditor/utilities/toggle.ts
437
+ const isBlockActive = (editor, format) => {
438
+ const [match] = Editor.nodes(editor, { match: (node) => Element.isElement(node) && node.type === format });
439
+ return Boolean(match);
440
+ };
441
+ const LIST_TYPES = /* @__PURE__ */ new Set(["numbered-list", "bulleted-list"]);
442
+ const toggleBlock = (editor, format) => {
443
+ const isActive = isBlockActive(editor, format);
444
+ const isList = LIST_TYPES.has(format);
445
+ Transforms.unwrapNodes(editor, {
446
+ match: (node) => Element.isElement(node) && isNotNil(node.type) && LIST_TYPES.has(node.type),
447
+ split: true
448
+ });
449
+ const formatting = isList ? "list-item" : format;
450
+ Transforms.setNodes(editor, { type: isActive ? "paragraph" : formatting });
451
+ if (!isActive && isList) {
452
+ const block = {
453
+ type: format,
454
+ children: []
455
+ };
456
+ Transforms.wrapNodes(editor, block);
457
+ }
458
+ };
459
+ const isMarkActive = (editor, format) => {
460
+ const valueMarks = Editor.marks(editor);
461
+ return isNonEmptyString(valueMarks && Object.keys(valueMarks).find((mark) => mark === format));
462
+ };
463
+ const toggleMark = (editor, format) => {
464
+ if (isMarkActive(editor, format)) Editor.removeMark(editor, format);
465
+ else Editor.addMark(editor, format, true);
466
+ };
467
+ //#endregion
468
+ //#region src/components/RichTextEditor/EditorButtons.tsx
469
+ const ButtonContainer = styled.div`
470
+ background: var(--rte-background, #ffffff);
471
+ border-bottom: 1px solid var(--rte-border-color, #c4c4c8);
472
+ display: flex;
473
+ gap: 8px;
474
+ padding: 8px 0;
475
+ position: sticky;
476
+ top: 0;
477
+
478
+ /* needs a stacking context to stay above RTE content */
479
+ z-index: 1;
480
+ `;
481
+ const EditorButton = ({ buttonType, disabled, format, icon, label, onCustomClick }) => {
482
+ const editor = useSlate();
483
+ const isActive = buttonType === "mark" ? isMarkActive(editor, format) : isBlockActive(editor, format);
484
+ const handleClick = (event) => {
485
+ event.preventDefault();
486
+ if (buttonType === "block") toggleBlock(editor, format);
487
+ else if (buttonType === "mark") toggleMark(editor, format);
488
+ if (onCustomClick) onCustomClick(event);
489
+ };
490
+ return /* @__PURE__ */ jsx(ToolbarButton, {
491
+ "aria-pressed": onCustomClick ? void 0 : isActive,
492
+ disabled,
493
+ icon: /* @__PURE__ */ jsx(Icon, { type: icon }),
494
+ isActive,
495
+ label,
496
+ onClick: handleClick
497
+ });
498
+ };
499
+ const FORMAT_BUTTONS = [
500
+ {
501
+ enabledFormat: "BOLD",
502
+ buttonType: "mark",
503
+ format: "bold",
504
+ icon: "bold",
505
+ label: "Bold"
506
+ },
507
+ {
508
+ enabledFormat: "ITALIC",
509
+ buttonType: "mark",
510
+ format: "italic",
511
+ icon: "italic",
512
+ label: "Italic"
513
+ },
514
+ {
515
+ enabledFormat: "HEADING_1",
516
+ buttonType: "block",
517
+ format: "heading-one",
518
+ icon: "heading",
519
+ label: "Heading 1"
520
+ },
521
+ {
522
+ enabledFormat: "HEADING_2",
523
+ buttonType: "block",
524
+ format: "heading-two",
525
+ icon: "heading-two",
526
+ label: "Heading 2"
527
+ },
528
+ {
529
+ enabledFormat: "BULLETED_LIST",
530
+ buttonType: "block",
531
+ format: "bulleted-list",
532
+ icon: "bulleted-list",
533
+ label: "Bulleted list"
534
+ },
535
+ {
536
+ enabledFormat: "NUMBERED_LIST",
537
+ buttonType: "block",
538
+ format: "numbered-list",
539
+ icon: "numbered-list",
540
+ label: "Numbered list"
541
+ },
542
+ {
543
+ enabledFormat: "BLOCKQUOTE",
544
+ buttonType: "block",
545
+ format: "blockquote",
546
+ icon: "block-quote",
547
+ label: "Block quote"
548
+ },
549
+ {
550
+ enabledFormat: "UNDERLINE",
551
+ buttonType: "mark",
552
+ format: "underline",
553
+ icon: "underline",
554
+ label: "Underline"
555
+ },
556
+ {
557
+ enabledFormat: "STRIKETHROUGH",
558
+ buttonType: "mark",
559
+ format: "strikethrough",
560
+ icon: "strikethrough",
561
+ label: "Strikethrough"
562
+ }
563
+ ];
564
+ const getSelectedText = (editor) => {
565
+ if (!editor.selection) return "";
566
+ const nodeFragment = Node.fragment(editor, editor.selection);
567
+ if (!nodeFragment[0]) return "";
568
+ return Node.string(nodeFragment[0]);
569
+ };
570
+ const noTextSelected = (editor) => {
571
+ return getSelectedText(editor).trim() === "";
572
+ };
573
+ const EditorButtons = ({ enabledFormats, disabled }) => {
574
+ const editor = useSlate();
575
+ return /* @__PURE__ */ jsxs(ButtonContainer, { children: [
576
+ FORMAT_BUTTONS.filter(({ enabledFormat }) => enabledFormats.includes(enabledFormat)).map(({ enabledFormat, ...buttonProps }) => /* @__PURE__ */ jsx(EditorButton, {
577
+ ...buttonProps,
578
+ disabled
579
+ }, enabledFormat)),
580
+ enabledFormats.includes(rteFormatTypes.LINK) && /* @__PURE__ */ jsx(EditorButton, {
581
+ buttonType: "insert",
582
+ disabled: disabled || noTextSelected(editor),
583
+ format: "link",
584
+ icon: "link",
585
+ label: "Link",
586
+ onCustomClick: () => {
587
+ insertLink(editor);
588
+ }
589
+ }),
590
+ enabledFormats.includes(rteFormatTypes.IMAGE) && /* @__PURE__ */ jsx(EditorButton, {
591
+ buttonType: "insert",
592
+ disabled,
593
+ format: "image",
594
+ icon: "image",
595
+ label: "Image",
596
+ onCustomClick: () => {
597
+ const promptUserForImageUrl = prompt("Enter the URL of the image:");
598
+ if (!isNonEmptyString(promptUserForImageUrl)) return;
599
+ if (!insertImage(editor, promptUserForImageUrl)) alert("The URL provided is not an image. Try again. Make sure to include the protocol (ex. https://) and a file type such as .png or .jpg in the URL.");
600
+ }
601
+ })
602
+ ] });
603
+ };
604
+ //#endregion
605
+ //#region src/components/RichTextEditor/Element.tsx
606
+ const Wrapper = styled.span`
607
+ display: inline-block;
608
+ user-select: none;
609
+ `;
610
+ const Image = styled.img`
611
+ box-shadow: ${({ $isHighlighted }) => $isHighlighted ? "0 0 0 3px var(--rte-accent-color, #2949e5)" : "none"};
612
+ max-height: 10em;
613
+ max-width: 100%;
614
+ user-select: none;
615
+ `;
616
+ const listStyle = css`
617
+ padding: 0 24px;
618
+
619
+ li {
620
+ font-size: 14px;
621
+ font-style: normal;
622
+ font-weight: 400;
623
+ margin: 8px 0;
624
+ }
625
+ `;
626
+ const EditorOrderedList = styled.ol`
627
+ ${listStyle}
628
+ `;
629
+ const EditorUnorderedList = styled.ul`
630
+ ${listStyle}
631
+ `;
632
+ const headerStyle = css`
633
+ font-weight: 500;
634
+ `;
635
+ const EditorHeading1 = styled.h1`
636
+ ${headerStyle}
637
+ font-size: 18px;
638
+ `;
639
+ const EditorHeading2 = styled.h2`
640
+ ${headerStyle}
641
+ font-size: 16px;
642
+ `;
643
+ const EditorHeading3 = styled.h3`
644
+ ${headerStyle}
645
+ font-size: 14px;
646
+ `;
647
+ const EditorParagraph = styled.p`
648
+ font-weight: 400;
649
+ `;
650
+ const EditorLink = styled.a`
651
+ color: var(--rte-accent-color, #2949e5);
652
+ font-weight: 300;
653
+ `;
654
+ const EditorSpan = styled.span`
655
+ font-weight: 400;
656
+ `;
657
+ const Element$1 = ({ attributes, children, element }) => {
658
+ const selected = useSelected();
659
+ const focused = useFocused();
660
+ switch (element.type) {
661
+ case "span": return /* @__PURE__ */ jsx(EditorSpan, {
662
+ ...attributes,
663
+ children
664
+ });
665
+ case "blockquote": return /* @__PURE__ */ jsx("blockquote", {
666
+ ...attributes,
667
+ children
668
+ });
669
+ case "bulleted-list": return /* @__PURE__ */ jsx(EditorUnorderedList, {
670
+ ...attributes,
671
+ children
672
+ });
673
+ case "heading-one": return /* @__PURE__ */ jsx(EditorHeading1, {
674
+ ...attributes,
675
+ children
676
+ });
677
+ case "heading-two": return /* @__PURE__ */ jsx(EditorHeading2, {
678
+ ...attributes,
679
+ children
680
+ });
681
+ case "heading-three":
682
+ case "heading-four":
683
+ case "heading-five":
684
+ case "heading-six": return /* @__PURE__ */ jsx(EditorHeading3, {
685
+ ...attributes,
686
+ children
687
+ });
688
+ case "list-item": return /* @__PURE__ */ jsx("li", {
689
+ ...attributes,
690
+ children
691
+ });
692
+ case "numbered-list": return /* @__PURE__ */ jsx(EditorOrderedList, {
693
+ ...attributes,
694
+ children
695
+ });
696
+ case "image": return /* @__PURE__ */ jsxs(Wrapper, {
697
+ ...attributes,
698
+ children: [/* @__PURE__ */ jsx(Wrapper, {
699
+ contentEditable: false,
700
+ children: /* @__PURE__ */ jsx(Image, {
701
+ alt: "",
702
+ contentEditable: false,
703
+ $isHighlighted: selected && focused,
704
+ src: element.url
705
+ })
706
+ }), children]
707
+ });
708
+ case "link": return /* @__PURE__ */ jsx(EditorLink, {
709
+ ...attributes,
710
+ href: element.url,
711
+ rel: "noopener noreferrer",
712
+ target: "_blank",
713
+ children
714
+ });
715
+ case "code": return /* @__PURE__ */ jsx("code", {
716
+ ...attributes,
717
+ children
718
+ });
719
+ case void 0:
720
+ default: return /* @__PURE__ */ jsx(EditorParagraph, {
721
+ ...attributes,
722
+ children
723
+ });
724
+ }
725
+ };
726
+ //#endregion
727
+ //#region src/components/RichTextEditor/Leaf.tsx
728
+ const Leaf = ({ attributes, children, leaf }) => {
729
+ let kids = children;
730
+ if (isNotNil(leaf.bold)) kids = /* @__PURE__ */ jsx("strong", { children: kids });
731
+ if (isNotNil(leaf.italic)) kids = /* @__PURE__ */ jsx("em", { children: kids });
732
+ if (isNotNil(leaf.underline)) kids = /* @__PURE__ */ jsx("u", { children: kids });
733
+ if (isNotNil(leaf.strikethrough)) kids = /* @__PURE__ */ jsx("s", { children: kids });
734
+ if (isNotNil(leaf.code)) kids = /* @__PURE__ */ jsx("code", { children: kids });
735
+ return /* @__PURE__ */ jsx("span", {
736
+ ...attributes,
737
+ children: kids
738
+ });
739
+ };
740
+ //#endregion
741
+ //#region src/helpers/escapeHtml/escapeHtml.ts
742
+ const matchHtmlRegExp = /["'&<>]/;
743
+ const doubleQuoteChar = 34;
744
+ const ampersandChar = 38;
745
+ const singleQuoteChar = 39;
746
+ const lessThanChar = 60;
747
+ const greaterThanChar = 62;
748
+ /**
749
+ * Handle special characters by escaping in the provided text string.
750
+ *
751
+ * @param {string} stringToEscape - the string to escape
752
+ * @return {string} - the escaped string
753
+ */
754
+ const escapeHtml = (stringToEscape) => {
755
+ const str = `${stringToEscape}`;
756
+ const match = matchHtmlRegExp.exec(str);
757
+ if (!match) return str;
758
+ let escape;
759
+ let html = "";
760
+ let index;
761
+ let lastIndex = 0;
762
+ for (index = match.index; index < str.length; index++) {
763
+ const charCode = str.codePointAt(index);
764
+ if (charCode === doubleQuoteChar) escape = "&quot;";
765
+ else if (charCode === ampersandChar) escape = "&amp;";
766
+ else if (charCode === singleQuoteChar) escape = "&#39;";
767
+ else if (charCode === lessThanChar) escape = "&lt;";
768
+ else if (charCode === greaterThanChar) escape = "&gt;";
769
+ else escape = "";
770
+ if (escape !== "") {
771
+ if (lastIndex !== index) html += str.slice(lastIndex, index);
772
+ lastIndex = index + 1;
773
+ html += escape;
774
+ }
775
+ }
776
+ return lastIndex !== index ? html + str.slice(lastIndex, index) : html;
777
+ };
778
+ //#endregion
779
+ //#region src/components/RichTextEditor/serializers.ts
780
+ const emptyTextNode = { text: "" };
781
+ const rteIsNonImageNodeType = (node) => node.type !== "image";
782
+ const rteIsInvalidImage = (node) => node.type === "image" && node.url === void 0;
783
+ const rteHasOnlyOneChild = (node) => isNotNil(node.children) && node.children.length === 1;
784
+ const rteIsEmptyTextNode = (node) => Text.isText(node) && node.text?.trim() === "";
785
+ const rteIsOnlyChildWithEmptyText = (node) => rteHasOnlyOneChild(node) && rteIsEmptyTextNode(node.children[0]);
786
+ const isValueNull = (value) => value.every((val) => isNil(val));
787
+ const sanitizedValue = (value) => isValueNull(value) ? null : value.join("");
788
+ const addAttrsToChildren = (child, attrs) => {
789
+ const childCopy = child;
790
+ if (childCopy.children) childCopy.children = childCopy.children.map((item) => {
791
+ return {
792
+ ...addAttrsToChildren(item, attrs),
793
+ ...attrs
794
+ };
795
+ });
796
+ return childCopy;
797
+ };
798
+ const serializeText = (textNode) => {
799
+ const newStringArr = [escapeHtml(textNode.text)];
800
+ if (isNotNil(textNode.bold) && textNode.bold) {
801
+ newStringArr.push("</strong>");
802
+ newStringArr.unshift("<strong>");
803
+ }
804
+ if (isNotNil(textNode.italic) && textNode.italic) {
805
+ newStringArr.push("</em>");
806
+ newStringArr.unshift("<em>");
807
+ }
808
+ if (isNotNil(textNode.underline) && textNode.underline) {
809
+ newStringArr.push("</u>");
810
+ newStringArr.unshift("<u>");
811
+ }
812
+ if (isNotNil(textNode.strikethrough) && textNode.strikethrough) {
813
+ newStringArr.push("</s>");
814
+ newStringArr.unshift("<s>");
815
+ }
816
+ if (isNotNil(textNode.code) && textNode.code) {
817
+ newStringArr.push("</code>");
818
+ newStringArr.unshift("<code>");
819
+ }
820
+ return newStringArr.join("");
821
+ };
822
+ const serializeNode = (node) => {
823
+ if (rteIsNonImageNodeType(node) && rteIsOnlyChildWithEmptyText(node) || rteIsInvalidImage(node)) return null;
824
+ if (Text.isText(node)) return serializeText(node);
825
+ const children = isNotNil(node.children) ? node.children.map((node_) => serializeNode(node_)).join("") : "";
826
+ switch (node.type) {
827
+ case "span": return `<span>${children}</span>`;
828
+ case "blockquote": return `<blockquote>${children}</blockquote>`;
829
+ case "paragraph": return `<p>${children}</p>`;
830
+ case "heading-one": return `<h1>${children}</h1>`;
831
+ case "heading-two": return `<h2>${children}</h2>`;
832
+ case "heading-three": return `<h3>${children}</h3>`;
833
+ case "heading-four": return `<h4>${children}</h4>`;
834
+ case "heading-five": return `<h5>${children}</h5>`;
835
+ case "heading-six": return `<h6>${children}</h6>`;
836
+ case "link": return `<a rel="noopener noreferrer" target="_blank" href="${escapeHtml(node.url)}">${children}</a>`;
837
+ case "numbered-list": return `<ol>${children}</ol>`;
838
+ case "bulleted-list": return `<ul>${children}</ul>`;
839
+ case "list-item": return `<li>${children}</li>`;
840
+ case "image": return `<img src="${escapeHtml(node.url)}" />`;
841
+ case "code": return `<code>${children}</code>`;
842
+ case void 0:
843
+ default: return children;
844
+ }
845
+ };
846
+ const rteSerializeToHTML = (value) => {
847
+ const mappedValue = value.map((node) => serializeNode(node));
848
+ return sanitizedValue(mappedValue);
849
+ };
850
+ const ELEMENT_TAGS = {
851
+ SPAN: () => ({ type: "span" }),
852
+ A: (el) => ({
853
+ type: "link",
854
+ url: el.getAttribute("href")
855
+ }),
856
+ BLOCKQUOTE: () => ({ type: "blockquote" }),
857
+ H1: () => ({ type: "heading-one" }),
858
+ H2: () => ({ type: "heading-two" }),
859
+ H3: () => ({ type: "heading-three" }),
860
+ H4: () => ({ type: "heading-four" }),
861
+ H5: () => ({ type: "heading-five" }),
862
+ H6: () => ({ type: "heading-six" }),
863
+ LI: () => ({ type: "list-item" }),
864
+ OL: () => ({ type: "numbered-list" }),
865
+ P: () => ({ type: "paragraph" }),
866
+ UL: () => ({ type: "bulleted-list" }),
867
+ IMG: (el) => ({
868
+ type: "image",
869
+ url: el.getAttribute("src")
870
+ }),
871
+ CODE: () => ({ type: "code" })
872
+ };
873
+ const TEXT_TAGS = {
874
+ DEL: () => ({ strikethrough: true }),
875
+ S: () => ({ strikethrough: true }),
876
+ EM: () => ({ italic: true }),
877
+ I: () => ({ italic: true }),
878
+ STRONG: () => ({ bold: true }),
879
+ U: () => ({ underline: true }),
880
+ CODE: () => ({ code: true })
881
+ };
882
+ const isTextNode = (el) => el.nodeType === 3;
883
+ const isElement = (el) => el.nodeType === 1;
884
+ const isCodeBlock = (element) => element.nodeName === "CODE";
885
+ const isInlineCodeBlock = (element, elementChildren) => isCodeBlock(element) && isNotNil(elementChildren[0]) && !/\n/.test(elementChildren[0]);
886
+ const getCodeChildren = (el) => [...el.childNodes].map((node) => {
887
+ if (isTextNode(node)) return node.textContent;
888
+ return node instanceof globalThis.Element ? node.outerHTML : void 0;
889
+ });
890
+ const isBlockElement = (el) => isElement(el) && el.nodeName in ELEMENT_TAGS && el.nodeName !== "A" && !(isCodeBlock(el) && isInlineCodeBlock(el, getCodeChildren(el)));
891
+ const isTextFormatElement = (el) => el.nodeName in TEXT_TAGS || el.nodeName === "A";
892
+ const parentHasBlock = /* @__PURE__ */ new WeakMap();
893
+ const hasBlockChild = (parent) => {
894
+ const saved = parentHasBlock.get(parent);
895
+ if (isNotNil(saved)) return saved;
896
+ const hasBlock = [...parent.childNodes].some((node) => isBlockElement(node));
897
+ parentHasBlock.set(parent, hasBlock);
898
+ return hasBlock;
899
+ };
900
+ const isWordSpace = (el) => {
901
+ const parent = el.parentNode;
902
+ if (isNil(parent)) return false;
903
+ if (parent.childNodes.length === 1) return isTextFormatElement(parent);
904
+ return !hasBlockChild(parent);
905
+ };
906
+ const shouldCreateBlock = (el, children) => !isInlineCodeBlock(el, children) && isNotNil(ELEMENT_TAGS[el.nodeName]);
907
+ const shouldCreateText = (el, children) => isInlineCodeBlock(el, children) || isNotNil(TEXT_TAGS[el.nodeName]);
908
+ const htmlChildIsEmptyText = (child) => isNotNil(child) && !isString(child) && child.text?.trim() === "";
909
+ const createEditorBlockElements = (children, el) => {
910
+ const { nodeName } = el;
911
+ if (nodeName !== "IMG" && children.length === 1 && !isString(children[0]) && isNotNil(children[0].text) && children[0].text?.trim() === "") return jsx$1("element", ELEMENT_TAGS.P(), emptyTextNode);
912
+ const attrs = ELEMENT_TAGS[nodeName](el);
913
+ return jsx$1("element", attrs, children);
914
+ };
915
+ const createEditorTextElements = (children, el) => {
916
+ const { nodeName } = el;
917
+ const attrs = TEXT_TAGS[nodeName]();
918
+ return children.map((child) => {
919
+ if (!isString(child) && isNotNil(child) && isNotNil(child.children)) return addAttrsToChildren(child, attrs);
920
+ if (htmlChildIsEmptyText(child)) return jsx$1("text", {}, emptyTextNode);
921
+ return jsx$1("text", attrs, child);
922
+ });
923
+ };
924
+ const blockWithOnlyTextChildren = (el) => el.nodeName === "BLOCKQUOTE" || el.nodeName === "OL" || el.nodeName === "UL";
925
+ const getNodeChildren = (el, mapper) => {
926
+ let children = [];
927
+ if (isCodeBlock(el)) children = getCodeChildren(el);
928
+ else if (blockWithOnlyTextChildren(el)) children = [...el.childNodes].filter((node) => isTextNode(node) || isTextFormatElement(node) || node.textContent?.trim() !== "").flatMap((node) => mapper(node));
929
+ else children = [...el.childNodes].flatMap((node) => mapper(node));
930
+ return children;
931
+ };
932
+ const deserializeHTMLNode = (el) => {
933
+ if (isTextNode(el)) {
934
+ if (el.nodeValue?.trim() === "" && !isWordSpace(el)) return null;
935
+ return el.textContent;
936
+ }
937
+ if (el.nodeName === "BR") return "\n";
938
+ if (!isElement(el)) return null;
939
+ const children = getNodeChildren(el, deserializeHTMLNode);
940
+ const nonNullChildren = children.filter((child) => child === null || typeof child === "string" || typeof child === "object" && ("text" in child || "type" in child)).filter((child) => isNotNil(child));
941
+ const safeChildren = nonNullChildren.length > 0 ? nonNullChildren : [emptyTextNode];
942
+ if (el.nodeName === "BODY") return jsx$1("fragment", {}, safeChildren);
943
+ if (shouldCreateBlock(el, children)) return createEditorBlockElements(safeChildren, el);
944
+ if (shouldCreateText(el, safeChildren)) return createEditorTextElements(safeChildren, el);
945
+ return children;
946
+ };
947
+ const isInlineNode = (node) => Text.isText(node) || Element.isElement(node) && node.type === "link";
948
+ const wrapLooseInlineNodes = (nodes) => {
949
+ const blocks = [];
950
+ let paragraph;
951
+ for (const node of nodes) if (!isInlineNode(node)) {
952
+ paragraph = void 0;
953
+ blocks.push(node);
954
+ } else if (isNil(paragraph)) {
955
+ paragraph = [node];
956
+ blocks.push({
957
+ type: "paragraph",
958
+ children: paragraph
959
+ });
960
+ } else paragraph.push(node);
961
+ return blocks;
962
+ };
963
+ const rteDeserializeHTML = (htmlString) => {
964
+ if (!htmlString) return;
965
+ const parsedEl = new DOMParser().parseFromString(htmlString, "text/html").body;
966
+ const body = deserializeHTMLNode(parsedEl);
967
+ const fragment = isArray(body) ? wrapLooseInlineNodes(body) : body;
968
+ const lastNodeOfFragment = isArray(fragment) ? fragment.at(-1) : void 0;
969
+ if (isArray(fragment) && Element.isElement(lastNodeOfFragment) && lastNodeOfFragment.type !== "paragraph") return [...fragment, {
970
+ type: "paragraph",
971
+ children: [{ text: "" }]
972
+ }];
973
+ return fragment;
974
+ };
975
+ //#endregion
976
+ //#region src/components/RichTextEditor/useLabelFor.ts
977
+ /**
978
+ * A `<label for>` only names and focuses native form controls, and the editor's text area is a
979
+ * contenteditable `div`. This finds the label that points at `id` and focuses the editor when the
980
+ * label is clicked. When `isName` is true, it also returns the label's id for `aria-labelledby`
981
+ * (it gives the label an id when it has none).
982
+ */
983
+ const useLabelFor = (editor, id, isName) => {
984
+ const generatedLabelId = useId();
985
+ const [labelId, setLabelId] = useState();
986
+ useEffect(() => {
987
+ if (!isNonEmptyString(id)) return;
988
+ const { ownerDocument } = ReactEditor.toDOMNode(editor, editor);
989
+ const label = [...ownerDocument.querySelectorAll("label")].find((element) => element.htmlFor === id);
990
+ if (!label) return;
991
+ const focusEditor = () => {
992
+ ReactEditor.focus(editor);
993
+ };
994
+ label.addEventListener("click", focusEditor);
995
+ const addsId = isName && !isNonEmptyString(label.id);
996
+ if (addsId) label.id = generatedLabelId;
997
+ if (isName) setLabelId(label.id);
998
+ return () => {
999
+ label.removeEventListener("click", focusEditor);
1000
+ if (addsId) label.removeAttribute("id");
1001
+ setLabelId(void 0);
1002
+ };
1003
+ }, [
1004
+ editor,
1005
+ generatedLabelId,
1006
+ id,
1007
+ isName
1008
+ ]);
1009
+ return labelId;
1010
+ };
1011
+ //#endregion
1012
+ //#region src/components/RichTextEditor/utilities/useExternalValueSync.ts
1013
+ const emptyValue = [{
1014
+ type: "paragraph",
1015
+ children: [{ text: "" }]
1016
+ }];
1017
+ const ECHO_WINDOW_MS = 1e4;
1018
+ const MAX_PENDING_ECHOES = 1e3;
1019
+ const prunePendingEchoes = (pending, now) => {
1020
+ for (const [html, emittedAt] of pending) {
1021
+ if (now - emittedAt <= ECHO_WINDOW_MS && pending.size <= MAX_PENDING_ECHOES) return;
1022
+ pending.delete(html);
1023
+ }
1024
+ };
1025
+ const hasContent = (nodes) => isNotNil(nodes) && nodes.some((node) => Text.isText(node) ? isNonEmptyString(node.text) : node.type === "image" || hasContent(node.children));
1026
+ /**
1027
+ * Keeps a Slate editor in sync with a controlled `value` prop without clobbering
1028
+ * in-progress typing.
1029
+ *
1030
+ * Controlled parents echo every `onChange` back as `value`. When that echo is
1031
+ * delayed (async store, debounced save, slow render), the prop can arrive
1032
+ * holding an *older* editor state than the one the user is typing into. The
1033
+ * previous implementation treated any mismatch as an external update, reset
1034
+ * `editor.children` to the stale value and nulled the selection — dropping
1035
+ * characters and sending the caret to the start of the document.
1036
+ *
1037
+ * Here, every state emitted through `onChange` is remembered for
1038
+ * `ECHO_WINDOW_MS`. A `value` that matches one of those states is an echo
1039
+ * (current or stale, in any order, any number of times, and even after an
1040
+ * external update) and is ignored. Anything else is a genuine external update
1041
+ * and is applied, as before.
1042
+ *
1043
+ * Returns the `onChange` handler to pass to `<Slate>`.
1044
+ *
1045
+ * This is directly from the Slate.js docs on how to update slate outside of onChange:
1046
+ * If you want to update the editor's content in response to events from outside of Slate, you
1047
+ * need to change the children property directly. The simplest way is to replace the value of
1048
+ * editor.children editor.children = newValue and trigger a re-rendering (e.g. by calling
1049
+ * editor.onChange()).
1050
+ * More info here: https://docs.slatejs.org/walkthroughs/06-saving-to-a-database
1051
+ */
1052
+ const useExternalValueSync = ({ editor, value, useExternalValue, onChange }) => {
1053
+ const lastSeenValueHtml = useRef(isUndefined(value) ? void 0 : rteSerializeToHTML(value));
1054
+ const lastSeenUseExternalValue = useRef(useExternalValue);
1055
+ const pendingEchoes = useRef(/* @__PURE__ */ new Map());
1056
+ const handleChange = useCallback((nodes) => {
1057
+ const html = rteSerializeToHTML(nodes);
1058
+ const pending = pendingEchoes.current;
1059
+ const now = Date.now();
1060
+ pending.delete(html);
1061
+ pending.set(html, now);
1062
+ prunePendingEchoes(pending, now);
1063
+ if (isNotNil(onChange)) onChange(nodes);
1064
+ }, [onChange]);
1065
+ useEffect(() => {
1066
+ if (isUndefined(value)) {
1067
+ lastSeenValueHtml.current = void 0;
1068
+ lastSeenUseExternalValue.current = useExternalValue;
1069
+ return;
1070
+ }
1071
+ const valueHtml = rteSerializeToHTML(value);
1072
+ if (valueHtml === lastSeenValueHtml.current && useExternalValue === lastSeenUseExternalValue.current) return;
1073
+ lastSeenValueHtml.current = valueHtml;
1074
+ lastSeenUseExternalValue.current = useExternalValue;
1075
+ const pending = pendingEchoes.current;
1076
+ prunePendingEchoes(pending, Date.now());
1077
+ if (pending.has(valueHtml)) return;
1078
+ /** if we have an external value and the text editor is not empty (and therefore valid), we can safely update the editor. */
1079
+ if (!(isEqual(value, emptyValue) || hasContent(value) && useExternalValue)) return;
1080
+ if (valueHtml === rteSerializeToHTML(editor.children)) return;
1081
+ editor.children = value;
1082
+ editor.selection = null;
1083
+ editor.onChange();
1084
+ }, [
1085
+ value,
1086
+ editor,
1087
+ useExternalValue
1088
+ ]);
1089
+ return handleChange;
1090
+ };
1091
+ //#endregion
1092
+ //#region src/components/RichTextEditor/RichTextEditor.tsx
1093
+ const EditableWrapper = styled(Editable)`
1094
+ color: var(--rte-text-color, #37373c);
1095
+ outline: none; /* the outer box shows the focus instead */
1096
+ `;
1097
+ const defaultHeight = "300px";
1098
+ const EditorWrapper = styled.div`
1099
+ background: var(--rte-background, #ffffff);
1100
+ border: 1px solid var(--rte-border-color, #c4c4c8);
1101
+ border-radius: var(--rte-border-radius, 4px);
1102
+ cursor: ${({ $disabled }) => $disabled ? "not-allowed" : "auto"};
1103
+
1104
+ /* UIProvider defines its font as a token, not on body. With no token, the font is inherited
1105
+ (VHSProvider sets it on body). */
1106
+ font-family: var(--rte-font-family, var(--wui-typography-family-default));
1107
+ font-size: 14px;
1108
+ font-weight: 500;
1109
+ height: ${({ $height }) => $height ?? defaultHeight};
1110
+ overflow: auto;
1111
+ padding: 0 16px;
1112
+
1113
+ &:has(${EditableWrapper}:focus) {
1114
+ outline: 2px solid var(--rte-focus-color, var(--rte-accent-color, #2949e5));
1115
+ outline-offset: -1px; /* on the border, where the sticky toolbar cannot cover it */
1116
+ }
1117
+
1118
+ strong {
1119
+ font-weight: 700;
1120
+ }
1121
+ `;
1122
+ const HOTKEYS = {
1123
+ "mod+b": "bold",
1124
+ "mod+i": "italic",
1125
+ "mod+u": "underline"
1126
+ };
1127
+ const toNodes = (content) => {
1128
+ if (!isString(content)) return content;
1129
+ const nodes = rteDeserializeHTML(content);
1130
+ return isArray(nodes) && nodes.length > 0 && nodes.every((node) => Element.isElement(node)) ? nodes : void 0;
1131
+ };
1132
+ const serialize = (nodes) => rteSerializeToHTML(nodes) ?? "";
1133
+ const RichTextEditor = ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabelledByAttribute, ariaLabelledBy, className, "data-testid": dataTestId = "rich-text-editor", defaultValue, disabled = false, enabledFormats = Object.values(rteFormatTypes), height, id, name, onBlur, onChange, placeholder = "Enter some rich text...", style, useExternalValue = false, value, ...ariaProps }) => {
1134
+ const renderElement = useCallback((props) => /* @__PURE__ */ jsx(Element$1, { ...props }), []);
1135
+ const renderLeaf = useCallback((props) => /* @__PURE__ */ jsx(Leaf, { ...props }), []);
1136
+ const editor = useMemo(() => {
1137
+ const baseEditor = withReact(createEditor());
1138
+ return withImages(withLinks(withHistory(baseEditor)));
1139
+ }, []);
1140
+ const [initialValue] = useState(() => value ?? toNodes(defaultValue) ?? emptyValue);
1141
+ const [html, setHtml] = useState(() => serialize(initialValue));
1142
+ const labelledBy = ariaLabelledByAttribute ?? ariaLabelledBy;
1143
+ const labelId = useLabelFor(editor, id, isNil(ariaLabel) && isNil(labelledBy));
1144
+ const externalValue = value ?? (isUndefined(defaultValue) ? emptyValue : void 0);
1145
+ const handleSyncedChange = useExternalValueSync({
1146
+ editor,
1147
+ value: externalValue,
1148
+ useExternalValue,
1149
+ onChange
1150
+ });
1151
+ const handleChange = (nodes) => {
1152
+ setHtml(serialize(nodes));
1153
+ handleSyncedChange(nodes);
1154
+ };
1155
+ const handleEditorBlur = (_event) => {
1156
+ const updatedValue = editor.children;
1157
+ if (isNotNil(onBlur)) onBlur(updatedValue);
1158
+ };
1159
+ const handleRemoveImageIfSelected = useCallback(() => {
1160
+ const { selection } = editor;
1161
+ if (isNil(selection) || isNil(selection.anchor.path[0])) return false;
1162
+ const selectedElement = editor.children[selection.anchor.path[0]];
1163
+ if (!Element.isElement(selectedElement) || selectedElement.type !== "image") return false;
1164
+ toggleBlock(editor, "image");
1165
+ Transforms.removeNodes(editor, { at: selection });
1166
+ Transforms.move(editor, { reverse: true });
1167
+ const spanElement = editor.children.find((child) => Element.isElement(child) && child.type === "span");
1168
+ if (Element.isElement(spanElement) && spanElement.type === "span" && isNotNil(spanElement.children) && isNotNil(spanElement.children[0]) && isNotNil(spanElement.children[0].text) && spanElement.children[0].text.trim() !== "") Transforms.setNodes(editor, { type: "paragraph" }, {
1169
+ match: (node) => Element.isElement(node) && node.type === "span" && isNotNil(node.children) && Text.isText(node.children[0]) && isNotNil(node.children[0].text) && node.children[0].text.trim() !== "",
1170
+ split: true,
1171
+ mode: "highest"
1172
+ });
1173
+ return true;
1174
+ }, [editor]);
1175
+ return /* @__PURE__ */ jsxs(EditorWrapper, {
1176
+ $disabled: disabled,
1177
+ $height: height,
1178
+ className,
1179
+ "data-testid": dataTestId,
1180
+ onClick: () => {
1181
+ ReactEditor.focus(editor);
1182
+ },
1183
+ style,
1184
+ children: [/* @__PURE__ */ jsxs(Slate, {
1185
+ editor,
1186
+ initialValue,
1187
+ onChange: handleChange,
1188
+ children: [/* @__PURE__ */ jsx(EditorButtons, {
1189
+ disabled,
1190
+ enabledFormats
1191
+ }), /* @__PURE__ */ jsx(EditableWrapper, {
1192
+ ...ariaProps,
1193
+ "aria-disabled": disabled || ariaProps["aria-disabled"],
1194
+ "aria-label": ariaLabel,
1195
+ "aria-labelledby": labelledBy ?? labelId,
1196
+ "aria-multiline": true,
1197
+ autoFocus: false,
1198
+ "data-testid": "editor-content",
1199
+ id,
1200
+ onBlur: handleEditorBlur,
1201
+ onKeyDown: (event) => {
1202
+ Object.keys(HOTKEYS).forEach((hotKey) => {
1203
+ if (isHotkey(hotKey, event)) {
1204
+ event.preventDefault();
1205
+ toggleMark(editor, HOTKEYS[hotKey]);
1206
+ }
1207
+ });
1208
+ if (event.key === "Enter" && !event.shiftKey) handleListEscape(editor);
1209
+ if (event.key === "Backspace" || event.key === "Clear" || event.key === "Delete") {
1210
+ if (handleRemoveImageIfSelected()) event.preventDefault();
1211
+ }
1212
+ },
1213
+ placeholder,
1214
+ readOnly: disabled,
1215
+ renderElement,
1216
+ renderLeaf,
1217
+ role: "textbox",
1218
+ spellCheck: true
1219
+ })]
1220
+ }), isNotNil(name) && /* @__PURE__ */ jsx("input", {
1221
+ disabled,
1222
+ name,
1223
+ type: "hidden",
1224
+ value: html
1225
+ })]
1226
+ });
1227
+ };
1228
+ RichTextEditor.displayName = "RichTextEditor";
1229
+ //#endregion
1230
+ export { RichTextEditor, rteDeserializeHTML, rteFormatTypes, rteSerializeToHTML };
1231
+
1232
+ //# sourceMappingURL=index.js.map