@exsto/ds 0.0.1-rc.6 → 0.0.1-rc.8
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/README.md +33 -0
- package/dist/chunks/toggle-C2c5oDaE.js +629 -0
- package/dist/chunks/toggle-C2c5oDaE.js.map +1 -0
- package/dist/ds/src/rich-text-editor.d.ts +8 -0
- package/dist/index.js +3 -470
- package/dist/index.js.map +1 -1
- package/dist/rich-text-editor.js +904 -0
- package/dist/rich-text-editor.js.map +1 -0
- package/dist/styles.css +159 -0
- package/dist/ui/src/components/edyn/rich-text-content/allow-list.d.ts +11 -0
- package/dist/ui/src/components/edyn/rich-text-content/index.d.ts +2 -0
- package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.d.ts +22 -0
- package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.test.d.ts +8 -0
- package/dist/ui/src/components/edyn/rich-text-editor/extensions.d.ts +18 -0
- package/dist/ui/src/components/edyn/rich-text-editor/index.d.ts +3 -0
- package/dist/ui/src/components/edyn/rich-text-editor/mention-list.d.ts +17 -0
- package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.d.ts +53 -0
- package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.test.d.ts +6 -0
- package/dist/ui/src/components/edyn/rich-text-editor/serialize.d.ts +13 -0
- package/dist/ui/src/components/edyn/rich-text-editor/sync.d.ts +19 -0
- package/dist/ui/src/components/edyn/rich-text-editor/sync.test.d.ts +11 -0
- package/dist/ui/src/components/edyn/rich-text-editor/toolbar.d.ts +20 -0
- package/dist/ui/src/components/edyn/rich-text-editor/types.d.ts +68 -0
- package/dist/ui/src/components/ui/popover/popover.d.ts +1 -1
- package/dist/ui/src/index.d.ts +1 -0
- package/package.json +21 -1
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import { B as cn, C as RICH_TEXT_ATTRIBUTES, E as isAllowedRichTextHref, L as PopoverTrigger, M as Popover, N as PopoverContent, O as popupItemClass, R as Button, T as RICH_TEXT_LINK_PROTOCOLS, _ as DropdownMenuTrigger, a as DropdownMenuContent, k as ScrollArea, n as toggleVariants, r as DropdownMenu, s as DropdownMenuItem, v as Input, w as RICH_TEXT_ELEMENTS, y as RICH_TEXT_BODY_CLASS } from "./chunks/toggle-C2c5oDaE.js";
|
|
2
|
+
import { useCallback, useEffect, useId, useImperativeHandle, useMemo, useReducer, useRef, useState } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { IconBold, IconItalic, IconLink, IconList, IconListNumbers, IconLoader2, IconSeparatorHorizontal, IconUnderline, IconVariable } from "@tabler/icons-react";
|
|
5
|
+
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
|
6
|
+
import { Extension, mergeAttributes } from "@tiptap/core";
|
|
7
|
+
import { Bold } from "@tiptap/extension-bold";
|
|
8
|
+
import { Document } from "@tiptap/extension-document";
|
|
9
|
+
import { HardBreak } from "@tiptap/extension-hard-break";
|
|
10
|
+
import { HorizontalRule } from "@tiptap/extension-horizontal-rule";
|
|
11
|
+
import { Italic } from "@tiptap/extension-italic";
|
|
12
|
+
import { Link } from "@tiptap/extension-link";
|
|
13
|
+
import { BulletList, ListItem, OrderedList } from "@tiptap/extension-list";
|
|
14
|
+
import { Mention } from "@tiptap/extension-mention";
|
|
15
|
+
import { Paragraph } from "@tiptap/extension-paragraph";
|
|
16
|
+
import { Text } from "@tiptap/extension-text";
|
|
17
|
+
import { Underline } from "@tiptap/extension-underline";
|
|
18
|
+
import { UndoRedo } from "@tiptap/extensions";
|
|
19
|
+
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
20
|
+
import { DOMSerializer } from "@tiptap/pm/model";
|
|
21
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
22
|
+
//#region ../ui/src/components/edyn/rich-text-editor/serialize.ts
|
|
23
|
+
var MENTION_MARK = "data-rich-text-mention";
|
|
24
|
+
var defaultMentionToText = (item) => `@${item.id}`;
|
|
25
|
+
/**
|
|
26
|
+
* The document as HTML. Not `editor.getHTML()`: that would write a mention as its chip element, and
|
|
27
|
+
* the HTML must carry the plain text the app chose (`@person@example.test`) and nothing else.
|
|
28
|
+
*/
|
|
29
|
+
var serializeDocument = (doc, toText) => {
|
|
30
|
+
const schema = doc.type.schema;
|
|
31
|
+
const nodes = {
|
|
32
|
+
...DOMSerializer.nodesFromSchema(schema),
|
|
33
|
+
mention: (node) => [
|
|
34
|
+
"span",
|
|
35
|
+
{ [MENTION_MARK]: "" },
|
|
36
|
+
toText({
|
|
37
|
+
id: String(node.attrs.id ?? ""),
|
|
38
|
+
label: String(node.attrs.label ?? "")
|
|
39
|
+
})
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
const serializer = new DOMSerializer(nodes, DOMSerializer.marksFromSchema(schema));
|
|
43
|
+
const container = document.createElement("div");
|
|
44
|
+
container.appendChild(serializer.serializeFragment(doc.content));
|
|
45
|
+
for (const element of Array.from(container.querySelectorAll(`[${MENTION_MARK}]`))) element.replaceWith(document.createTextNode(element.textContent ?? ""));
|
|
46
|
+
return container.innerHTML;
|
|
47
|
+
};
|
|
48
|
+
/** No text and no mention. A document of blank paragraphs, spaces and line breaks is empty too. */
|
|
49
|
+
var isDocumentEmpty = (doc) => {
|
|
50
|
+
if (doc.textContent.trim() !== "") return false;
|
|
51
|
+
let hasMention = false;
|
|
52
|
+
doc.descendants((node) => {
|
|
53
|
+
if (node.type.name === "mention") hasMention = true;
|
|
54
|
+
return !hasMention;
|
|
55
|
+
});
|
|
56
|
+
return !hasMention;
|
|
57
|
+
};
|
|
58
|
+
/** Characters the HTML text will have: a mention counts as the text it is written as. */
|
|
59
|
+
var countCharacters = (doc, toText) => {
|
|
60
|
+
let total = 0;
|
|
61
|
+
doc.descendants((node) => {
|
|
62
|
+
if (node.isText) total += node.text?.length ?? 0;
|
|
63
|
+
else if (node.type.name === "mention") total += toText({
|
|
64
|
+
id: String(node.attrs.id ?? ""),
|
|
65
|
+
label: String(node.attrs.label ?? "")
|
|
66
|
+
}).length;
|
|
67
|
+
else if (node.type.name === "hardBreak") total += 1;
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
return total;
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../ui/src/components/edyn/rich-text-editor/extensions.ts
|
|
74
|
+
var MENTION_CHIP_CLASS = "rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary";
|
|
75
|
+
var limitKey = new PluginKey("richTextMaxLength");
|
|
76
|
+
/** Refuses an edit that would take the text over the limit (a paste included) and lets one that shortens it through. */
|
|
77
|
+
var maxLengthExtension = (host) => Extension.create({
|
|
78
|
+
name: "maxLength",
|
|
79
|
+
addProseMirrorPlugins() {
|
|
80
|
+
return [new Plugin({
|
|
81
|
+
key: limitKey,
|
|
82
|
+
filterTransaction: (tr, state) => {
|
|
83
|
+
const limit = host.maxLength();
|
|
84
|
+
if (!tr.docChanged || limit === void 0) return true;
|
|
85
|
+
const next = countCharacters(tr.doc, host.toText());
|
|
86
|
+
return next <= limit || next <= countCharacters(state.doc, host.toText());
|
|
87
|
+
}
|
|
88
|
+
})];
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* The editor's whole schema: paragraph, line break, bold, italic, underline, bullet and numbered lists, link, and the
|
|
93
|
+
* mention chip. Whatever else arrives (a paste, `value`) is not in the schema, so ProseMirror drops the markup and keeps
|
|
94
|
+
* the text.
|
|
95
|
+
*/
|
|
96
|
+
var buildExtensions = (host) => [
|
|
97
|
+
Document,
|
|
98
|
+
Paragraph,
|
|
99
|
+
Text,
|
|
100
|
+
HardBreak,
|
|
101
|
+
Bold,
|
|
102
|
+
Italic,
|
|
103
|
+
Underline,
|
|
104
|
+
BulletList,
|
|
105
|
+
OrderedList.extend({ addAttributes() {
|
|
106
|
+
return {};
|
|
107
|
+
} }),
|
|
108
|
+
ListItem,
|
|
109
|
+
HorizontalRule,
|
|
110
|
+
Link.extend({ addAttributes() {
|
|
111
|
+
return { href: {
|
|
112
|
+
default: null,
|
|
113
|
+
parseHTML: (element) => element.getAttribute("href"),
|
|
114
|
+
renderHTML: (attributes) => attributes.href ? { href: attributes.href } : {}
|
|
115
|
+
} };
|
|
116
|
+
} }).configure({
|
|
117
|
+
openOnClick: false,
|
|
118
|
+
autolink: true,
|
|
119
|
+
linkOnPaste: true,
|
|
120
|
+
defaultProtocol: "https",
|
|
121
|
+
protocols: ["mailto"],
|
|
122
|
+
isAllowedUri: (url) => isAllowedRichTextHref(url),
|
|
123
|
+
HTMLAttributes: {
|
|
124
|
+
rel: null,
|
|
125
|
+
target: null,
|
|
126
|
+
class: null
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
UndoRedo,
|
|
130
|
+
maxLengthExtension(host),
|
|
131
|
+
Mention.extend({ addAttributes() {
|
|
132
|
+
return {
|
|
133
|
+
id: {
|
|
134
|
+
default: null,
|
|
135
|
+
parseHTML: (element) => element.getAttribute("data-id"),
|
|
136
|
+
renderHTML: (attributes) => attributes.id ? { "data-id": attributes.id } : {}
|
|
137
|
+
},
|
|
138
|
+
label: {
|
|
139
|
+
default: null,
|
|
140
|
+
parseHTML: (element) => element.getAttribute("data-label"),
|
|
141
|
+
renderHTML: (attributes) => attributes.label ? { "data-label": attributes.label } : {}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
} }).configure({
|
|
145
|
+
HTMLAttributes: { class: MENTION_CHIP_CLASS },
|
|
146
|
+
renderText: ({ node }) => `@${node.attrs.label ?? node.attrs.id}`,
|
|
147
|
+
renderHTML: ({ options, node }) => [
|
|
148
|
+
"span",
|
|
149
|
+
mergeAttributes(options.HTMLAttributes),
|
|
150
|
+
`@${node.attrs.label ?? node.attrs.id}`
|
|
151
|
+
],
|
|
152
|
+
suggestion: {
|
|
153
|
+
char: "@",
|
|
154
|
+
allow: () => host.mentionEnabled(),
|
|
155
|
+
items: () => [],
|
|
156
|
+
command: ({ editor, range, props }) => {
|
|
157
|
+
editor.chain().focus().insertContentAt(range, [{
|
|
158
|
+
type: "mention",
|
|
159
|
+
attrs: {
|
|
160
|
+
id: props.id,
|
|
161
|
+
label: props.label
|
|
162
|
+
}
|
|
163
|
+
}, {
|
|
164
|
+
type: "text",
|
|
165
|
+
text: " "
|
|
166
|
+
}]).run();
|
|
167
|
+
},
|
|
168
|
+
render: () => ({
|
|
169
|
+
onStart: (props) => host.onSuggestion(props),
|
|
170
|
+
onUpdate: (props) => host.onSuggestion(props),
|
|
171
|
+
onKeyDown: (props) => host.onSuggestionKey(props),
|
|
172
|
+
onExit: () => host.onSuggestion(null)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
];
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region ../ui/src/components/edyn/rich-text-editor/mention-list.tsx
|
|
179
|
+
var FIRST = {
|
|
180
|
+
status: "loading",
|
|
181
|
+
items: [],
|
|
182
|
+
page: 1,
|
|
183
|
+
hasMore: false,
|
|
184
|
+
more: false,
|
|
185
|
+
active: 0
|
|
186
|
+
};
|
|
187
|
+
var toPage = (result) => Array.isArray(result) ? {
|
|
188
|
+
items: result,
|
|
189
|
+
hasMore: false
|
|
190
|
+
} : result;
|
|
191
|
+
var optionId = (listId, index) => `${listId}-${index}`;
|
|
192
|
+
/** The list that opens under the caret when the writer types @. */
|
|
193
|
+
var MentionList = ({ query, rect, config, labels, listId, onPick, keyHandler, onActiveChange }) => {
|
|
194
|
+
const [state, setState] = useState(FIRST);
|
|
195
|
+
const refSearch = useRef(config.search);
|
|
196
|
+
refSearch.current = config.search;
|
|
197
|
+
const refFirst = useRef(true);
|
|
198
|
+
const refController = useRef(null);
|
|
199
|
+
const refViewport = useRef(null);
|
|
200
|
+
const debounce = config.debounce ?? 250;
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
refController.current = controller;
|
|
204
|
+
setState((previous) => ({
|
|
205
|
+
...previous,
|
|
206
|
+
status: "loading",
|
|
207
|
+
page: 1,
|
|
208
|
+
more: false
|
|
209
|
+
}));
|
|
210
|
+
const timer = setTimeout(async () => {
|
|
211
|
+
try {
|
|
212
|
+
const page = toPage(await refSearch.current(query, {
|
|
213
|
+
page: 1,
|
|
214
|
+
signal: controller.signal
|
|
215
|
+
}));
|
|
216
|
+
if (controller.signal.aborted) return;
|
|
217
|
+
setState({
|
|
218
|
+
status: "ready",
|
|
219
|
+
items: page.items,
|
|
220
|
+
page: 1,
|
|
221
|
+
hasMore: page.hasMore === true,
|
|
222
|
+
more: false,
|
|
223
|
+
active: 0
|
|
224
|
+
});
|
|
225
|
+
} catch {
|
|
226
|
+
if (!controller.signal.aborted) setState((previous) => ({
|
|
227
|
+
...previous,
|
|
228
|
+
status: "error",
|
|
229
|
+
items: [],
|
|
230
|
+
hasMore: false
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
}, refFirst.current ? 0 : debounce);
|
|
234
|
+
refFirst.current = false;
|
|
235
|
+
return () => {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
controller.abort();
|
|
238
|
+
};
|
|
239
|
+
}, [query, debounce]);
|
|
240
|
+
const loadMore = () => {
|
|
241
|
+
const controller = refController.current;
|
|
242
|
+
if (!controller || state.status !== "ready" || !state.hasMore || state.more) return;
|
|
243
|
+
const next = state.page + 1;
|
|
244
|
+
setState((previous) => ({
|
|
245
|
+
...previous,
|
|
246
|
+
more: true
|
|
247
|
+
}));
|
|
248
|
+
refSearch.current(query, {
|
|
249
|
+
page: next,
|
|
250
|
+
signal: controller.signal
|
|
251
|
+
}).then((result) => {
|
|
252
|
+
if (controller.signal.aborted) return;
|
|
253
|
+
const page = toPage(result);
|
|
254
|
+
setState((previous) => ({
|
|
255
|
+
...previous,
|
|
256
|
+
items: [...previous.items, ...page.items.filter((item) => !previous.items.some((known) => known.id === item.id))],
|
|
257
|
+
page: next,
|
|
258
|
+
hasMore: page.hasMore === true,
|
|
259
|
+
more: false
|
|
260
|
+
}));
|
|
261
|
+
}).catch(() => {
|
|
262
|
+
if (!controller.signal.aborted) setState((previous) => ({
|
|
263
|
+
...previous,
|
|
264
|
+
more: false,
|
|
265
|
+
hasMore: false
|
|
266
|
+
}));
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
const refLoadMore = useRef(loadMore);
|
|
270
|
+
refLoadMore.current = loadMore;
|
|
271
|
+
const activeItem = state.items[state.active];
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
onActiveChange(activeItem ? optionId(listId, state.active) : void 0);
|
|
274
|
+
}, [
|
|
275
|
+
activeItem,
|
|
276
|
+
listId,
|
|
277
|
+
onActiveChange,
|
|
278
|
+
state.active
|
|
279
|
+
]);
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
document.getElementById(optionId(listId, state.active))?.scrollIntoView({ block: "nearest" });
|
|
282
|
+
}, [listId, state.active]);
|
|
283
|
+
const watchSentinel = useCallback((node) => {
|
|
284
|
+
if (!node) return;
|
|
285
|
+
const observer = new IntersectionObserver((entries) => {
|
|
286
|
+
if (entries.some((entry) => entry.isIntersecting)) refLoadMore.current();
|
|
287
|
+
}, { root: refViewport.current });
|
|
288
|
+
observer.observe(node);
|
|
289
|
+
return () => observer.disconnect();
|
|
290
|
+
}, []);
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
keyHandler.current = (event) => {
|
|
293
|
+
const count = state.items.length;
|
|
294
|
+
if (event.key === "Escape") {
|
|
295
|
+
event.stopPropagation();
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (count === 0) return false;
|
|
299
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
300
|
+
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
301
|
+
const next = (state.active + step + count) % count;
|
|
302
|
+
if (step === 1 && state.active === count - 1) refLoadMore.current();
|
|
303
|
+
setState((previous) => ({
|
|
304
|
+
...previous,
|
|
305
|
+
active: next
|
|
306
|
+
}));
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
if ((event.key === "Enter" || event.key === "Tab") && activeItem) {
|
|
310
|
+
onPick(activeItem);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
};
|
|
315
|
+
return () => {
|
|
316
|
+
keyHandler.current = null;
|
|
317
|
+
};
|
|
318
|
+
}, [
|
|
319
|
+
activeItem,
|
|
320
|
+
keyHandler,
|
|
321
|
+
onPick,
|
|
322
|
+
state.active,
|
|
323
|
+
state.items.length
|
|
324
|
+
]);
|
|
325
|
+
const anchor = { getBoundingClientRect: () => rect?.() ?? new DOMRect(0, 0, 0, 0) };
|
|
326
|
+
const announcement = state.status === "loading" ? labels.mentionSearching : state.status === "error" ? labels.mentionError : state.items.length === 0 ? labels.mentionEmpty : labels.mentionResults.replace("{count}", String(state.items.length));
|
|
327
|
+
return /* @__PURE__ */ jsx(Popover, {
|
|
328
|
+
open: true,
|
|
329
|
+
onOpenChange: () => void 0,
|
|
330
|
+
children: /* @__PURE__ */ jsxs(PopoverContent, {
|
|
331
|
+
anchor,
|
|
332
|
+
align: "start",
|
|
333
|
+
side: "bottom",
|
|
334
|
+
sideOffset: 6,
|
|
335
|
+
initialFocus: false,
|
|
336
|
+
finalFocus: false,
|
|
337
|
+
"data-slot": "rich-text-mention-popup",
|
|
338
|
+
className: "w-72 gap-0 p-1",
|
|
339
|
+
children: [/* @__PURE__ */ jsxs(ScrollArea, {
|
|
340
|
+
className: "max-h-60",
|
|
341
|
+
viewportRef: refViewport,
|
|
342
|
+
children: [
|
|
343
|
+
/* @__PURE__ */ jsx("div", {
|
|
344
|
+
role: "listbox",
|
|
345
|
+
id: listId,
|
|
346
|
+
"aria-label": labels.mentionList,
|
|
347
|
+
className: cn("flex flex-col gap-px", state.items.length === 0 && "hidden"),
|
|
348
|
+
children: state.items.map((item, index) => /* @__PURE__ */ jsxs("div", {
|
|
349
|
+
id: optionId(listId, index),
|
|
350
|
+
role: "option",
|
|
351
|
+
"aria-selected": index === state.active,
|
|
352
|
+
"data-slot": "rich-text-mention-option",
|
|
353
|
+
"data-highlighted": index === state.active ? "" : void 0,
|
|
354
|
+
className: cn(popupItemClass("base-ui"), "h-auto min-h-8 flex-col items-start gap-0"),
|
|
355
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
356
|
+
onMouseMove: () => setState((previous) => previous.active === index ? previous : {
|
|
357
|
+
...previous,
|
|
358
|
+
active: index
|
|
359
|
+
}),
|
|
360
|
+
onClick: () => onPick(item),
|
|
361
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
362
|
+
className: "truncate",
|
|
363
|
+
children: item.label
|
|
364
|
+
}), item.secondary ? /* @__PURE__ */ jsx("span", {
|
|
365
|
+
className: "truncate text-xs text-foreground/70",
|
|
366
|
+
children: item.secondary
|
|
367
|
+
}) : null]
|
|
368
|
+
}, item.id))
|
|
369
|
+
}),
|
|
370
|
+
state.status === "loading" && state.items.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
371
|
+
className: "flex items-center gap-2 px-2 py-1.5 text-sm text-muted-foreground",
|
|
372
|
+
children: [/* @__PURE__ */ jsx(IconLoader2, {
|
|
373
|
+
className: "size-4 animate-spin motion-reduce:animate-none",
|
|
374
|
+
"aria-hidden": true
|
|
375
|
+
}), labels.mentionSearching]
|
|
376
|
+
}) : null,
|
|
377
|
+
state.status === "ready" && state.items.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
378
|
+
className: "px-2 py-1.5 text-sm text-muted-foreground",
|
|
379
|
+
children: labels.mentionEmpty
|
|
380
|
+
}) : null,
|
|
381
|
+
state.status === "error" ? /* @__PURE__ */ jsx("div", {
|
|
382
|
+
className: "px-2 py-1.5 text-sm text-destructive",
|
|
383
|
+
children: labels.mentionError
|
|
384
|
+
}) : null,
|
|
385
|
+
state.hasMore ? /* @__PURE__ */ jsx("div", {
|
|
386
|
+
ref: watchSentinel,
|
|
387
|
+
className: "flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground",
|
|
388
|
+
children: state.more ? labels.mentionLoadingMore : null
|
|
389
|
+
}, state.items.length) : null
|
|
390
|
+
]
|
|
391
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
392
|
+
role: "status",
|
|
393
|
+
className: "sr-only",
|
|
394
|
+
children: announcement
|
|
395
|
+
})]
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region ../ui/src/components/edyn/rich-text-editor/sync.ts
|
|
401
|
+
/**
|
|
402
|
+
* Whether commands may be called on the editor. TipTap builds an editor during the render, mounts its view later, and
|
|
403
|
+
* destroys one that has not been mounted within a moment (useEditor then makes a new one), so an effect can be handed an
|
|
404
|
+
* editor that is not there yet or no longer is. `isDestroyed` is true for both: it reads true until the view exists. A
|
|
405
|
+
* destroyed editor has no command manager, so `editor.commands` throws on it.
|
|
406
|
+
*/
|
|
407
|
+
var isLive = (editor) => editor.isInitialized && !editor.isDestroyed;
|
|
408
|
+
/**
|
|
409
|
+
* Brings the editor to a value the parent passed in, if it is one the editor did not just report itself.
|
|
410
|
+
* `known` is the last HTML the editor and the parent agree on (the starting value, then whatever the editor emitted or was
|
|
411
|
+
* given). The caret stays where it was, kept inside the new content, so a value changed from outside while the writer is
|
|
412
|
+
* typing does not throw the caret to the start.
|
|
413
|
+
*/
|
|
414
|
+
var syncValue = (editor, value, known) => {
|
|
415
|
+
if (!isLive(editor) || value === void 0 || value === known.current) return;
|
|
416
|
+
known.current = value;
|
|
417
|
+
const { from, to } = editor.state.selection;
|
|
418
|
+
editor.commands.setContent(value, { emitUpdate: false });
|
|
419
|
+
editor.commands.command(({ tr }) => {
|
|
420
|
+
const size = tr.doc.content.size;
|
|
421
|
+
tr.setSelection(TextSelection.between(tr.doc.resolve(Math.min(from, size)), tr.doc.resolve(Math.min(to, size))));
|
|
422
|
+
return true;
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
/** Sets whether the editor can be edited without telling the app something changed (TipTap emits an update by default). */
|
|
426
|
+
var syncEditable = (editor, editable) => {
|
|
427
|
+
if (isLive(editor) && editor.isEditable !== editable) editor.setEditable(editable, false);
|
|
428
|
+
};
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region ../ui/src/components/edyn/rich-text-editor/toolbar.tsx
|
|
431
|
+
var BUTTON_CLASS = cn(toggleVariants({
|
|
432
|
+
variant: "default",
|
|
433
|
+
size: "sm"
|
|
434
|
+
}), "size-7 px-0");
|
|
435
|
+
/** A typed address becomes a link: "name@example.test" a mailto, "example.test" an https address. */
|
|
436
|
+
var normalizeHref = (raw) => {
|
|
437
|
+
const value = raw.trim();
|
|
438
|
+
if (value === "" || /^[a-z][a-z0-9+.-]*:/i.test(value)) return value;
|
|
439
|
+
return /^[^\s@/]+@[^\s@/]+\.[^\s@/]+$/.test(value) ? `mailto:${value}` : `https://${value}`;
|
|
440
|
+
};
|
|
441
|
+
var ToolbarButton = ({ label, pressed, disabled, onClick, children }) => /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
442
|
+
type: "button",
|
|
443
|
+
"aria-label": label,
|
|
444
|
+
title: label,
|
|
445
|
+
"aria-pressed": pressed,
|
|
446
|
+
"data-pressed": pressed ? "" : void 0,
|
|
447
|
+
disabled,
|
|
448
|
+
className: BUTTON_CLASS,
|
|
449
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
450
|
+
onClick,
|
|
451
|
+
children
|
|
452
|
+
});
|
|
453
|
+
var hrefAt = (editor, range) => {
|
|
454
|
+
const { doc } = editor.state;
|
|
455
|
+
const marks = range.from < range.to ? doc.nodeAt(range.from)?.marks ?? [] : doc.resolve(range.from).marks();
|
|
456
|
+
return String(marks.find((mark) => mark.type.name === "link")?.attrs.href ?? "");
|
|
457
|
+
};
|
|
458
|
+
var LinkControl = ({ range, editor, labels, active, disabled, open, onOpenChange }) => {
|
|
459
|
+
const [address, setAddress] = useState("");
|
|
460
|
+
const [invalid, setInvalid] = useState(false);
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
if (open) {
|
|
463
|
+
setAddress(range.current ? hrefAt(editor, range.current) : "");
|
|
464
|
+
setInvalid(false);
|
|
465
|
+
}
|
|
466
|
+
}, [
|
|
467
|
+
editor,
|
|
468
|
+
open,
|
|
469
|
+
range
|
|
470
|
+
]);
|
|
471
|
+
const apply = (event) => {
|
|
472
|
+
event.preventDefault();
|
|
473
|
+
event.stopPropagation();
|
|
474
|
+
const href = normalizeHref(address);
|
|
475
|
+
if (!isAllowedRichTextHref(href)) {
|
|
476
|
+
setInvalid(true);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const saved = range.current ?? {
|
|
480
|
+
from: editor.state.selection.from,
|
|
481
|
+
to: editor.state.selection.to
|
|
482
|
+
};
|
|
483
|
+
const chain = editor.chain().focus().setTextSelection(saved);
|
|
484
|
+
if (saved.from === saved.to && !hrefAt(editor, saved)) chain.insertContent({
|
|
485
|
+
type: "text",
|
|
486
|
+
text: href,
|
|
487
|
+
marks: [{
|
|
488
|
+
type: "link",
|
|
489
|
+
attrs: { href }
|
|
490
|
+
}]
|
|
491
|
+
}).run();
|
|
492
|
+
else chain.extendMarkRange("link").setLink({ href }).run();
|
|
493
|
+
onOpenChange(false);
|
|
494
|
+
};
|
|
495
|
+
const remove = () => {
|
|
496
|
+
const saved = range.current ?? {
|
|
497
|
+
from: editor.state.selection.from,
|
|
498
|
+
to: editor.state.selection.to
|
|
499
|
+
};
|
|
500
|
+
editor.chain().focus().setTextSelection(saved).extendMarkRange("link").unsetLink().run();
|
|
501
|
+
onOpenChange(false);
|
|
502
|
+
};
|
|
503
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
504
|
+
open,
|
|
505
|
+
onOpenChange,
|
|
506
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
507
|
+
render: /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
508
|
+
type: "button",
|
|
509
|
+
"aria-label": labels.link,
|
|
510
|
+
title: labels.link,
|
|
511
|
+
"aria-pressed": active,
|
|
512
|
+
"data-pressed": active ? "" : void 0,
|
|
513
|
+
disabled,
|
|
514
|
+
className: BUTTON_CLASS
|
|
515
|
+
}),
|
|
516
|
+
children: /* @__PURE__ */ jsx(IconLink, { "aria-hidden": true })
|
|
517
|
+
}), /* @__PURE__ */ jsx(PopoverContent, {
|
|
518
|
+
align: "start",
|
|
519
|
+
className: "w-80",
|
|
520
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
521
|
+
className: "flex flex-col gap-2",
|
|
522
|
+
onSubmit: apply,
|
|
523
|
+
children: [
|
|
524
|
+
/* @__PURE__ */ jsx(Input, {
|
|
525
|
+
type: "text",
|
|
526
|
+
inputMode: "url",
|
|
527
|
+
"aria-label": labels.linkAddress,
|
|
528
|
+
"aria-invalid": invalid || void 0,
|
|
529
|
+
value: address,
|
|
530
|
+
onChange: (event) => {
|
|
531
|
+
setAddress(event.target.value);
|
|
532
|
+
setInvalid(false);
|
|
533
|
+
}
|
|
534
|
+
}),
|
|
535
|
+
invalid ? /* @__PURE__ */ jsx("p", {
|
|
536
|
+
className: "text-xs text-destructive",
|
|
537
|
+
children: labels.linkInvalid
|
|
538
|
+
}) : null,
|
|
539
|
+
/* @__PURE__ */ jsxs("div", {
|
|
540
|
+
className: "flex justify-end gap-2",
|
|
541
|
+
children: [active ? /* @__PURE__ */ jsx(Button, {
|
|
542
|
+
type: "button",
|
|
543
|
+
variant: "outline",
|
|
544
|
+
size: "sm",
|
|
545
|
+
onClick: remove,
|
|
546
|
+
children: labels.linkRemove
|
|
547
|
+
}) : null, /* @__PURE__ */ jsx(Button, {
|
|
548
|
+
type: "submit",
|
|
549
|
+
size: "sm",
|
|
550
|
+
children: labels.linkApply
|
|
551
|
+
})]
|
|
552
|
+
})
|
|
553
|
+
]
|
|
554
|
+
})
|
|
555
|
+
})]
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
var RichTextToolbar = ({ linkRange, editor, labels, fields, disabled, linkOpen, onLinkOpenChange }) => {
|
|
559
|
+
const state = useEditorState({
|
|
560
|
+
editor,
|
|
561
|
+
selector: ({ editor: current }) => ({
|
|
562
|
+
bold: current.isActive("bold"),
|
|
563
|
+
italic: current.isActive("italic"),
|
|
564
|
+
underline: current.isActive("underline"),
|
|
565
|
+
bullets: current.isActive("bulletList"),
|
|
566
|
+
numbers: current.isActive("orderedList"),
|
|
567
|
+
link: current.isActive("link")
|
|
568
|
+
})
|
|
569
|
+
});
|
|
570
|
+
return /* @__PURE__ */ jsxs(Toolbar.Root, {
|
|
571
|
+
"data-slot": "rich-text-toolbar",
|
|
572
|
+
"aria-label": labels.toolbar,
|
|
573
|
+
className: "flex flex-wrap items-center gap-0.5 border-b border-input p-1",
|
|
574
|
+
children: [
|
|
575
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
576
|
+
label: labels.bold,
|
|
577
|
+
pressed: state.bold,
|
|
578
|
+
disabled,
|
|
579
|
+
onClick: () => editor.chain().focus().toggleBold().run(),
|
|
580
|
+
children: /* @__PURE__ */ jsx(IconBold, { "aria-hidden": true })
|
|
581
|
+
}),
|
|
582
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
583
|
+
label: labels.italic,
|
|
584
|
+
pressed: state.italic,
|
|
585
|
+
disabled,
|
|
586
|
+
onClick: () => editor.chain().focus().toggleItalic().run(),
|
|
587
|
+
children: /* @__PURE__ */ jsx(IconItalic, { "aria-hidden": true })
|
|
588
|
+
}),
|
|
589
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
590
|
+
label: labels.underline,
|
|
591
|
+
pressed: state.underline,
|
|
592
|
+
disabled,
|
|
593
|
+
onClick: () => editor.chain().focus().toggleUnderline().run(),
|
|
594
|
+
children: /* @__PURE__ */ jsx(IconUnderline, { "aria-hidden": true })
|
|
595
|
+
}),
|
|
596
|
+
/* @__PURE__ */ jsx(Toolbar.Separator, { className: "mx-1 h-4 w-px bg-border" }),
|
|
597
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
598
|
+
label: labels.bulletList,
|
|
599
|
+
pressed: state.bullets,
|
|
600
|
+
disabled,
|
|
601
|
+
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
|
602
|
+
children: /* @__PURE__ */ jsx(IconList, { "aria-hidden": true })
|
|
603
|
+
}),
|
|
604
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
605
|
+
label: labels.numberedList,
|
|
606
|
+
pressed: state.numbers,
|
|
607
|
+
disabled,
|
|
608
|
+
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
|
609
|
+
children: /* @__PURE__ */ jsx(IconListNumbers, { "aria-hidden": true })
|
|
610
|
+
}),
|
|
611
|
+
/* @__PURE__ */ jsx(LinkControl, {
|
|
612
|
+
range: linkRange,
|
|
613
|
+
editor,
|
|
614
|
+
labels,
|
|
615
|
+
active: state.link,
|
|
616
|
+
disabled,
|
|
617
|
+
open: linkOpen,
|
|
618
|
+
onOpenChange: onLinkOpenChange
|
|
619
|
+
}),
|
|
620
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
621
|
+
label: labels.horizontalRule,
|
|
622
|
+
disabled,
|
|
623
|
+
onClick: () => editor.chain().focus().setHorizontalRule().run(),
|
|
624
|
+
children: /* @__PURE__ */ jsx(IconSeparatorHorizontal, { "aria-hidden": true })
|
|
625
|
+
}),
|
|
626
|
+
fields && fields.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Toolbar.Separator, { className: "mx-1 h-4 w-px bg-border" }), /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
627
|
+
render: /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
628
|
+
type: "button",
|
|
629
|
+
"aria-label": labels.insertField,
|
|
630
|
+
title: labels.insertField,
|
|
631
|
+
disabled,
|
|
632
|
+
className: BUTTON_CLASS
|
|
633
|
+
}),
|
|
634
|
+
children: /* @__PURE__ */ jsx(IconVariable, { "aria-hidden": true })
|
|
635
|
+
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
636
|
+
align: "start",
|
|
637
|
+
className: "w-56",
|
|
638
|
+
children: fields.map((field) => /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
639
|
+
onClick: () => editor.chain().focus().insertContent({
|
|
640
|
+
type: "text",
|
|
641
|
+
text: `{{${field.id}}}`
|
|
642
|
+
}).run(),
|
|
643
|
+
children: field.label
|
|
644
|
+
}, field.id))
|
|
645
|
+
})] })] }) : null
|
|
646
|
+
]
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region ../ui/src/components/edyn/rich-text-editor/types.ts
|
|
651
|
+
var DEFAULT_RICH_TEXT_LABELS = {
|
|
652
|
+
toolbar: "Formatting",
|
|
653
|
+
bold: "Bold",
|
|
654
|
+
italic: "Italic",
|
|
655
|
+
underline: "Underline",
|
|
656
|
+
bulletList: "Bulleted list",
|
|
657
|
+
numberedList: "Numbered list",
|
|
658
|
+
link: "Link",
|
|
659
|
+
horizontalRule: "Horizontal line",
|
|
660
|
+
insertField: "Insert a field",
|
|
661
|
+
linkAddress: "Link address",
|
|
662
|
+
linkApply: "Apply",
|
|
663
|
+
linkRemove: "Remove link",
|
|
664
|
+
linkInvalid: "Enter an http, https or mailto address.",
|
|
665
|
+
mentionList: "Mentions",
|
|
666
|
+
mentionSearching: "Searching...",
|
|
667
|
+
mentionEmpty: "No matches",
|
|
668
|
+
mentionError: "The search failed",
|
|
669
|
+
mentionLoadingMore: "Loading more...",
|
|
670
|
+
mentionResults: "{count} results"
|
|
671
|
+
};
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region ../ui/src/components/edyn/rich-text-editor/rich-text-editor.tsx
|
|
674
|
+
var LINE = 1.5;
|
|
675
|
+
/**
|
|
676
|
+
* A small rich-text field: paragraphs, bold, italic, underline, bulleted and numbered lists and links, with optional
|
|
677
|
+
* @mentions the app answers asynchronously. The value is HTML in and out, and only the elements in `RICH_TEXT_ELEMENTS`
|
|
678
|
+
* can come out (no class, no style), whatever is pasted. A mention is a chip while editing and plain text in the HTML.
|
|
679
|
+
* The server should still sanitise: this is a convenience for the writer, not a security boundary.
|
|
680
|
+
*/
|
|
681
|
+
var RichTextEditor = ({ value, defaultValue = "", onChange, placeholder, disabled = false, readOnly = false, invalid = false, minRows = 6, maxRows = 20, maxLength, mention, fields, toolbar = true, labels, id, autoFocus = false, onPasteFiles, ref, className, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...props }) => {
|
|
682
|
+
const isInvalid = invalid || ariaInvalid === true || ariaInvalid === "true";
|
|
683
|
+
const text = useMemo(() => ({
|
|
684
|
+
...DEFAULT_RICH_TEXT_LABELS,
|
|
685
|
+
...labels
|
|
686
|
+
}), [labels]);
|
|
687
|
+
const listId = useId();
|
|
688
|
+
const [suggestion, setSuggestion] = useState(null);
|
|
689
|
+
const [activeOption, setActiveOption] = useState(void 0);
|
|
690
|
+
const [linkOpen, setLinkOpen] = useState(false);
|
|
691
|
+
const refLinkRange = useRef(null);
|
|
692
|
+
const refKeys = useRef(null);
|
|
693
|
+
const refKnown = useRef(value ?? defaultValue);
|
|
694
|
+
const refPending = useRef([]);
|
|
695
|
+
const [, wake] = useReducer((count) => count + 1, 0);
|
|
696
|
+
const editable = !disabled && !readOnly;
|
|
697
|
+
const showToolbar = toolbar && !readOnly;
|
|
698
|
+
const latest = useRef({
|
|
699
|
+
mention,
|
|
700
|
+
maxLength,
|
|
701
|
+
onChange,
|
|
702
|
+
onPasteFiles,
|
|
703
|
+
toolbar: showToolbar
|
|
704
|
+
});
|
|
705
|
+
latest.current = {
|
|
706
|
+
mention,
|
|
707
|
+
maxLength,
|
|
708
|
+
onChange,
|
|
709
|
+
onPasteFiles,
|
|
710
|
+
toolbar: showToolbar
|
|
711
|
+
};
|
|
712
|
+
const host = useMemo(() => ({
|
|
713
|
+
mentionEnabled: () => latest.current.mention !== void 0,
|
|
714
|
+
maxLength: () => latest.current.maxLength,
|
|
715
|
+
toText: () => latest.current.mention?.toText ?? defaultMentionToText,
|
|
716
|
+
onSuggestion: (props) => {
|
|
717
|
+
if (props === null) setActiveOption(void 0);
|
|
718
|
+
setSuggestion(props === null ? null : {
|
|
719
|
+
query: props.query,
|
|
720
|
+
rect: props.clientRect ?? null,
|
|
721
|
+
command: props.command
|
|
722
|
+
});
|
|
723
|
+
},
|
|
724
|
+
onSuggestionKey: ({ event }) => refKeys.current?.(event) ?? false
|
|
725
|
+
}), []);
|
|
726
|
+
const extensions = useMemo(() => buildExtensions(host), [host]);
|
|
727
|
+
const emit = useCallback((current) => {
|
|
728
|
+
const html = isDocumentEmpty(current.state.doc) ? "" : serializeDocument(current.state.doc, host.toText());
|
|
729
|
+
refKnown.current = html;
|
|
730
|
+
return html;
|
|
731
|
+
}, [host]);
|
|
732
|
+
const attributes = useMemo(() => {
|
|
733
|
+
const entries = {
|
|
734
|
+
role: "textbox",
|
|
735
|
+
"aria-multiline": "true",
|
|
736
|
+
"aria-autocomplete": mention ? "list" : void 0,
|
|
737
|
+
"aria-controls": suggestion ? listId : void 0,
|
|
738
|
+
"aria-activedescendant": suggestion ? activeOption : void 0,
|
|
739
|
+
"aria-label": ariaLabel,
|
|
740
|
+
"aria-labelledby": ariaLabelledBy,
|
|
741
|
+
"aria-describedby": ariaDescribedBy,
|
|
742
|
+
"aria-invalid": isInvalid ? "true" : void 0,
|
|
743
|
+
"aria-readonly": readOnly ? "true" : void 0,
|
|
744
|
+
"aria-disabled": disabled ? "true" : void 0,
|
|
745
|
+
"aria-placeholder": placeholder,
|
|
746
|
+
tabindex: readOnly && !disabled ? "0" : void 0,
|
|
747
|
+
id,
|
|
748
|
+
"data-slot": "rich-text-editor-content",
|
|
749
|
+
class: cn(RICH_TEXT_BODY_CLASS, "min-h-(--rich-text-min) px-2.5 py-2 outline-none")
|
|
750
|
+
};
|
|
751
|
+
return Object.fromEntries(Object.entries(entries).filter((entry) => entry[1] !== void 0));
|
|
752
|
+
}, [
|
|
753
|
+
activeOption,
|
|
754
|
+
ariaDescribedBy,
|
|
755
|
+
ariaLabel,
|
|
756
|
+
ariaLabelledBy,
|
|
757
|
+
disabled,
|
|
758
|
+
id,
|
|
759
|
+
isInvalid,
|
|
760
|
+
listId,
|
|
761
|
+
mention,
|
|
762
|
+
placeholder,
|
|
763
|
+
readOnly,
|
|
764
|
+
suggestion
|
|
765
|
+
]);
|
|
766
|
+
const editor = useEditor({
|
|
767
|
+
extensions,
|
|
768
|
+
content: value ?? defaultValue,
|
|
769
|
+
editable,
|
|
770
|
+
autofocus: autoFocus ? "end" : false,
|
|
771
|
+
editorProps: {
|
|
772
|
+
attributes,
|
|
773
|
+
handlePaste: (_view, event) => {
|
|
774
|
+
const data = event.clipboardData;
|
|
775
|
+
if (!(data !== null && data.files.length > 0 && data.getData("text/plain") === "" && data.getData("text/html") === "") || !latest.current.onPasteFiles) return false;
|
|
776
|
+
latest.current.onPasteFiles(Array.from(data.files));
|
|
777
|
+
return true;
|
|
778
|
+
},
|
|
779
|
+
handleKeyDown: (view, event) => {
|
|
780
|
+
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "k" && latest.current.toolbar) {
|
|
781
|
+
event.preventDefault();
|
|
782
|
+
refLinkRange.current = {
|
|
783
|
+
from: view.state.selection.from,
|
|
784
|
+
to: view.state.selection.to
|
|
785
|
+
};
|
|
786
|
+
setLinkOpen(true);
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
onBlur: () => {
|
|
793
|
+
setSuggestion(null);
|
|
794
|
+
setActiveOption(void 0);
|
|
795
|
+
},
|
|
796
|
+
onUpdate: ({ editor: current }) => {
|
|
797
|
+
latest.current.onChange?.(emit(current));
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
const ready = isLive(editor);
|
|
801
|
+
useEffect(() => {
|
|
802
|
+
editor.on("create", wake);
|
|
803
|
+
wake();
|
|
804
|
+
return () => {
|
|
805
|
+
editor.off("create", wake);
|
|
806
|
+
};
|
|
807
|
+
}, [editor]);
|
|
808
|
+
const whenLive = useCallback((action) => {
|
|
809
|
+
if (isLive(editor)) action(editor);
|
|
810
|
+
else refPending.current.push(action);
|
|
811
|
+
}, [editor]);
|
|
812
|
+
useImperativeHandle(ref, () => ({
|
|
813
|
+
insertText: (text) => {
|
|
814
|
+
whenLive((live) => {
|
|
815
|
+
live.chain().focus().insertContent({
|
|
816
|
+
type: "text",
|
|
817
|
+
text
|
|
818
|
+
}).run();
|
|
819
|
+
});
|
|
820
|
+
},
|
|
821
|
+
focus: () => {
|
|
822
|
+
whenLive((live) => {
|
|
823
|
+
live.commands.focus();
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}), [whenLive]);
|
|
827
|
+
useEffect(() => {
|
|
828
|
+
if (!ready || !isLive(editor)) return;
|
|
829
|
+
for (const action of refPending.current.splice(0)) action(editor);
|
|
830
|
+
}, [editor, ready]);
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
if (ready) syncEditable(editor, editable);
|
|
833
|
+
}, [
|
|
834
|
+
editable,
|
|
835
|
+
editor,
|
|
836
|
+
ready
|
|
837
|
+
]);
|
|
838
|
+
useEffect(() => {
|
|
839
|
+
if (ready) syncValue(editor, value, refKnown);
|
|
840
|
+
}, [
|
|
841
|
+
editor,
|
|
842
|
+
ready,
|
|
843
|
+
value
|
|
844
|
+
]);
|
|
845
|
+
const empty = useEditorState({
|
|
846
|
+
editor,
|
|
847
|
+
selector: ({ editor: current }) => isDocumentEmpty(current.state.doc)
|
|
848
|
+
});
|
|
849
|
+
const pick = useCallback((item) => {
|
|
850
|
+
suggestion?.command(item);
|
|
851
|
+
}, [suggestion]);
|
|
852
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
853
|
+
"data-slot": "rich-text-editor",
|
|
854
|
+
"data-invalid": isInvalid ? "" : void 0,
|
|
855
|
+
"data-disabled": disabled ? "" : void 0,
|
|
856
|
+
"data-readonly": readOnly ? "" : void 0,
|
|
857
|
+
className: cn("flex w-full min-w-0 flex-col rounded-lg border border-input bg-transparent text-base transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:bg-input/50 data-disabled:opacity-50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:data-disabled:bg-input/80 dark:data-invalid:ring-destructive/40", className),
|
|
858
|
+
...props,
|
|
859
|
+
children: [
|
|
860
|
+
showToolbar ? /* @__PURE__ */ jsx(RichTextToolbar, {
|
|
861
|
+
editor,
|
|
862
|
+
labels: text,
|
|
863
|
+
fields,
|
|
864
|
+
disabled,
|
|
865
|
+
linkOpen,
|
|
866
|
+
linkRange: refLinkRange,
|
|
867
|
+
onLinkOpenChange: (next) => {
|
|
868
|
+
if (next && !linkOpen) refLinkRange.current = {
|
|
869
|
+
from: editor.state.selection.from,
|
|
870
|
+
to: editor.state.selection.to
|
|
871
|
+
};
|
|
872
|
+
setLinkOpen(next);
|
|
873
|
+
}
|
|
874
|
+
}) : null,
|
|
875
|
+
/* @__PURE__ */ jsxs(ScrollArea, {
|
|
876
|
+
className: "relative",
|
|
877
|
+
style: {
|
|
878
|
+
maxHeight: `${maxRows * LINE + 1}rem`,
|
|
879
|
+
["--rich-text-min"]: `${minRows * LINE + 1}rem`
|
|
880
|
+
},
|
|
881
|
+
focusable: false,
|
|
882
|
+
children: [empty && placeholder ? /* @__PURE__ */ jsx("p", {
|
|
883
|
+
"aria-hidden": true,
|
|
884
|
+
className: "pointer-events-none absolute start-2.5 top-2 text-muted-foreground",
|
|
885
|
+
children: placeholder
|
|
886
|
+
}) : null, /* @__PURE__ */ jsx(EditorContent, { editor })]
|
|
887
|
+
}),
|
|
888
|
+
suggestion && mention ? /* @__PURE__ */ jsx(MentionList, {
|
|
889
|
+
query: suggestion.query,
|
|
890
|
+
rect: suggestion.rect,
|
|
891
|
+
config: mention,
|
|
892
|
+
labels: text,
|
|
893
|
+
listId,
|
|
894
|
+
onPick: pick,
|
|
895
|
+
keyHandler: refKeys,
|
|
896
|
+
onActiveChange: setActiveOption
|
|
897
|
+
}) : null
|
|
898
|
+
]
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
//#endregion
|
|
902
|
+
export { DEFAULT_RICH_TEXT_LABELS, RICH_TEXT_ATTRIBUTES, RICH_TEXT_ELEMENTS, RICH_TEXT_LINK_PROTOCOLS, RichTextEditor };
|
|
903
|
+
|
|
904
|
+
//# sourceMappingURL=rich-text-editor.js.map
|