@edux-design/comments 0.1.4 → 0.3.0
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/CHANGELOG.md +24 -0
- package/README.md +20 -0
- package/dist/index.d.mts +163 -48
- package/dist/index.d.ts +163 -48
- package/dist/index.js +424 -156
- package/dist/index.mjs +425 -149
- package/docs/composer.md +65 -0
- package/docs/mention-portals.md +105 -0
- package/package.json +11 -24
- package/src/demos/Comment.stories.jsx +0 -593
- package/src/demos/CommentComposer.stories.jsx +0 -66
- package/src/elements/Comment.jsx +0 -240
- package/src/elements/Comment.test.jsx +0 -151
- package/src/elements/CommentComposer.jsx +0 -184
- package/src/elements/CommentComposer.test.jsx +0 -125
- package/src/elements/CommentMention.jsx +0 -232
- package/src/elements/CommentMention.test.jsx +0 -150
- package/src/elements/CommentMentionExtension.jsx +0 -56
- package/src/index.js +0 -3
- package/tsconfig.json +0 -10
package/dist/index.mjs
CHANGED
|
@@ -21,7 +21,10 @@ var CommentContext = createContext({
|
|
|
21
21
|
resolved: false
|
|
22
22
|
});
|
|
23
23
|
var useCommentContext = () => useContext(CommentContext);
|
|
24
|
-
var
|
|
24
|
+
var flattenReplies = (children) => React.Children.toArray(children).flatMap(
|
|
25
|
+
(child) => isValidElement(child) && child.type === React.Fragment ? flattenReplies(child.props.children) : [child]
|
|
26
|
+
);
|
|
27
|
+
function CommentComponent({
|
|
25
28
|
author = "Author",
|
|
26
29
|
timestamp,
|
|
27
30
|
avatar,
|
|
@@ -39,10 +42,7 @@ var Comment = forwardRef(function Comment2({
|
|
|
39
42
|
}, ref) {
|
|
40
43
|
const childArray = React.Children.toArray(children);
|
|
41
44
|
const actionsSlot = childArray.find(
|
|
42
|
-
(child) =>
|
|
43
|
-
var _a;
|
|
44
|
-
return isValidElement(child) && (child.type === CommentActions || ((_a = child.type) == null ? void 0 : _a.displayName) === "Comment.Actions");
|
|
45
|
-
}
|
|
45
|
+
(child) => isValidElement(child) && (child.type === CommentActions || child.type?.displayName === "Comment.Actions")
|
|
46
46
|
);
|
|
47
47
|
const bodyNodes = childArray.filter((child) => child !== actionsSlot);
|
|
48
48
|
const bodyContent = body ?? bodyNodes;
|
|
@@ -50,9 +50,11 @@ var Comment = forwardRef(function Comment2({
|
|
|
50
50
|
const isResolved = resolved || variant === "resolved";
|
|
51
51
|
const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
|
|
52
52
|
const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
|
|
53
|
-
const shouldRenderAvatar = Boolean(
|
|
53
|
+
const shouldRenderAvatar = Boolean(
|
|
54
|
+
avatar || avatarProps || resolvedAvatarName
|
|
55
|
+
);
|
|
54
56
|
const replyItems = useMemo(
|
|
55
|
-
() =>
|
|
57
|
+
() => flattenReplies(replies).filter(
|
|
56
58
|
(reply) => reply !== null && reply !== void 0 && reply !== false
|
|
57
59
|
),
|
|
58
60
|
[replies]
|
|
@@ -64,49 +66,55 @@ var Comment = forwardRef(function Comment2({
|
|
|
64
66
|
setShowAllReplies(!shouldCollapseReplies);
|
|
65
67
|
}, [shouldCollapseReplies]);
|
|
66
68
|
const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
|
|
67
|
-
return /* @__PURE__ */ jsx(
|
|
68
|
-
|
|
69
|
+
return /* @__PURE__ */ jsx(
|
|
70
|
+
CommentContext.Provider,
|
|
69
71
|
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
72
|
+
value: { editable, onResolve, resolved: isResolved },
|
|
73
|
+
children: /* @__PURE__ */ jsxs(
|
|
74
|
+
"div",
|
|
75
|
+
{
|
|
76
|
+
ref,
|
|
77
|
+
className: cx(
|
|
78
|
+
"flex gap-12 rounded-default border p-12",
|
|
79
|
+
resolvedVariant,
|
|
80
|
+
className
|
|
81
|
+
),
|
|
82
|
+
"data-variant": variant,
|
|
83
|
+
...props,
|
|
84
|
+
children: [
|
|
85
|
+
shouldRenderAvatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx(Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
|
|
86
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
87
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [
|
|
88
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
|
|
89
|
+
timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-fg-subtle", children: timestamp })
|
|
90
|
+
] }),
|
|
91
|
+
hasBodyContent && !isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
|
|
92
|
+
isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
|
|
93
|
+
actionsSlot ? /* @__PURE__ */ jsx("div", { className: "mt-8", children: actionsSlot }) : null,
|
|
94
|
+
replies && !isResolved ? /* @__PURE__ */ jsxs("div", { className: "mt-[40px] border-l border-border-minimal pl-[20px]", children: [
|
|
95
|
+
shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ jsxs(
|
|
96
|
+
"button",
|
|
97
|
+
{
|
|
98
|
+
type: "button",
|
|
99
|
+
className: "mb-8 text-xs font-semibold text-fg-info-base",
|
|
100
|
+
onClick: () => setShowAllReplies(true),
|
|
101
|
+
"aria-expanded": showAllReplies,
|
|
102
|
+
children: [
|
|
103
|
+
"Show all ",
|
|
104
|
+
replyCount,
|
|
105
|
+
" replies"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
visibleReplies
|
|
110
|
+
] }) : null
|
|
111
|
+
] })
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
)
|
|
107
115
|
}
|
|
108
|
-
)
|
|
109
|
-
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
110
118
|
var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ jsx(
|
|
111
119
|
"div",
|
|
112
120
|
{
|
|
@@ -166,10 +174,13 @@ var CommentResolve = ({
|
|
|
166
174
|
}
|
|
167
175
|
);
|
|
168
176
|
};
|
|
169
|
-
Comment
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
var Comment = Object.assign(forwardRef(CommentComponent), {
|
|
178
|
+
Actions: CommentActions,
|
|
179
|
+
Reply: CommentReply,
|
|
180
|
+
Edit: CommentEdit,
|
|
181
|
+
Resolve: CommentResolve
|
|
182
|
+
});
|
|
183
|
+
Comment.displayName = "Comment";
|
|
173
184
|
CommentActions.displayName = "Comment.Actions";
|
|
174
185
|
CommentReply.displayName = "Comment.Reply";
|
|
175
186
|
CommentEdit.displayName = "Comment.Edit";
|
|
@@ -182,7 +193,9 @@ import React3, {
|
|
|
182
193
|
useRef,
|
|
183
194
|
createContext as createContext2,
|
|
184
195
|
useContext as useContext2,
|
|
185
|
-
useEffect as useEffect2
|
|
196
|
+
useEffect as useEffect2,
|
|
197
|
+
useImperativeHandle,
|
|
198
|
+
useState as useState2
|
|
186
199
|
} from "react";
|
|
187
200
|
import { cx as cx2 } from "@edux-design/utils";
|
|
188
201
|
import { Avatar as Avatar2 } from "@edux-design/avatars";
|
|
@@ -198,9 +211,8 @@ import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
|
|
|
198
211
|
import { Mention } from "@edux-design/mentions";
|
|
199
212
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
200
213
|
var MentionNodeView = ({ node }) => {
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
|
|
214
|
+
const handle = node.attrs?.handle ?? "";
|
|
215
|
+
const label = node.attrs?.label ?? "";
|
|
204
216
|
return /* @__PURE__ */ jsx2(
|
|
205
217
|
NodeViewWrapper,
|
|
206
218
|
{
|
|
@@ -243,6 +255,11 @@ var CommentMentionExtension = Node.create({
|
|
|
243
255
|
label
|
|
244
256
|
];
|
|
245
257
|
},
|
|
258
|
+
renderText({ node }) {
|
|
259
|
+
const handle = node.attrs.handle || "";
|
|
260
|
+
const label = node.attrs.label || "";
|
|
261
|
+
return `@${handle || label || "mention"}`;
|
|
262
|
+
},
|
|
246
263
|
addNodeView() {
|
|
247
264
|
return ReactNodeViewRenderer(MentionNodeView);
|
|
248
265
|
}
|
|
@@ -260,87 +277,237 @@ var useCommentComposer = () => {
|
|
|
260
277
|
}
|
|
261
278
|
return context;
|
|
262
279
|
};
|
|
263
|
-
|
|
280
|
+
function readValue(editor) {
|
|
281
|
+
const mentions = [];
|
|
282
|
+
editor.state.doc.descendants((node) => {
|
|
283
|
+
if (node.type.name === "commentMention") {
|
|
284
|
+
mentions.push({
|
|
285
|
+
id: node.attrs.id ?? null,
|
|
286
|
+
handle: node.attrs.handle ?? "",
|
|
287
|
+
label: node.attrs.label ?? ""
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
const legacyText = editor.getText();
|
|
292
|
+
const text = editor.getText({
|
|
293
|
+
blockSeparator: "\n",
|
|
294
|
+
textSerializers: {
|
|
295
|
+
commentMention: ({ node }) => {
|
|
296
|
+
const { handle = "", label = "" } = node.attrs;
|
|
297
|
+
return label ? `@${handle} \xB7 ${label}` : `@${handle}`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}).replace(/\u00a0/g, " ").replace(/[\u200b-\u200d\ufeff]/g, "").replace(/\r?\n/g, " ");
|
|
301
|
+
return {
|
|
302
|
+
text,
|
|
303
|
+
legacyText,
|
|
304
|
+
html: editor.getHTML(),
|
|
305
|
+
json: editor.getJSON(),
|
|
306
|
+
mentions,
|
|
307
|
+
isEmpty: !text.trim()
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function stringContent(value, format) {
|
|
311
|
+
if (format !== "text") return value;
|
|
312
|
+
return {
|
|
313
|
+
type: "doc",
|
|
314
|
+
content: value.split(/\r?\n/).map((line) => ({
|
|
315
|
+
type: "paragraph",
|
|
316
|
+
...line ? { content: [{ type: "text", text: line }] } : {}
|
|
317
|
+
}))
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function CommentComposerComponent({
|
|
264
321
|
value,
|
|
322
|
+
valueFormat = "html",
|
|
323
|
+
richValue,
|
|
265
324
|
onChange,
|
|
325
|
+
onValueChange,
|
|
266
326
|
onSubmit,
|
|
267
327
|
onCancel,
|
|
328
|
+
composerRef,
|
|
329
|
+
editorLabel = "Comment",
|
|
330
|
+
editorAttributes: suppliedEditorAttributes,
|
|
331
|
+
autoFocus = false,
|
|
332
|
+
externalSubmit = false,
|
|
333
|
+
submitOnModEnter = false,
|
|
268
334
|
placeholder = "Write a comment...",
|
|
269
335
|
rows = 4,
|
|
270
336
|
disabled = false,
|
|
271
337
|
isSubmitting = false,
|
|
272
338
|
submitLabel = "Post",
|
|
339
|
+
submittingLabel = "Posting...",
|
|
273
340
|
cancelLabel = "Cancel",
|
|
274
341
|
avatar,
|
|
275
342
|
avatarProps,
|
|
276
343
|
actions,
|
|
277
344
|
className,
|
|
278
345
|
children,
|
|
346
|
+
onKeyDown,
|
|
347
|
+
onKeyDownCapture,
|
|
279
348
|
...props
|
|
280
349
|
}, ref) {
|
|
281
|
-
const
|
|
282
|
-
const
|
|
350
|
+
const containerRef = useRef(null);
|
|
351
|
+
const formRef = useRef(null);
|
|
352
|
+
const pendingFocusRef = useRef(null);
|
|
353
|
+
const handledShortcutEvents = useRef(/* @__PURE__ */ new WeakSet());
|
|
354
|
+
const callbacks = useRef({ onChange, onValueChange });
|
|
355
|
+
callbacks.current = { onChange, onValueChange };
|
|
356
|
+
const pendingEchoesRef = useRef([]);
|
|
357
|
+
const controlledRef = useRef(false);
|
|
358
|
+
controlledRef.current = value !== void 0 || richValue !== void 0;
|
|
359
|
+
const placeholderRef = useRef(placeholder);
|
|
360
|
+
placeholderRef.current = placeholder;
|
|
361
|
+
const [hasValue, setHasValue] = useState2(Boolean(richValue || value?.trim()));
|
|
362
|
+
const locked = disabled || isSubmitting;
|
|
363
|
+
const editorAttributes = useMemo2(
|
|
364
|
+
() => ({
|
|
365
|
+
...suppliedEditorAttributes,
|
|
366
|
+
"data-comment-editor": "true",
|
|
367
|
+
role: "textbox",
|
|
368
|
+
"aria-label": editorLabel,
|
|
369
|
+
"aria-multiline": "true",
|
|
370
|
+
"aria-disabled": String(locked),
|
|
371
|
+
class: cx2(
|
|
372
|
+
"w-full bg-bg-base text-fg-base border-2 rounded-md border-border-base",
|
|
373
|
+
"outline-none transition-all duration-300 ease-in-out motion-reduce:transition-none",
|
|
374
|
+
"pl-[20px] py-12 font-normal text-base leading-relaxed overflow-y-auto",
|
|
375
|
+
"focus:shadow-focus focus-visible:shadow-focus",
|
|
376
|
+
locked && "bg-bg-subtle border-border-subtle text-fg-inactive"
|
|
377
|
+
),
|
|
378
|
+
style: `height: ${Math.max(1, rows) * 30}px`
|
|
379
|
+
}),
|
|
380
|
+
[editorLabel, locked, rows, suppliedEditorAttributes]
|
|
381
|
+
);
|
|
382
|
+
const isSubmitDisabled = locked || !hasValue;
|
|
283
383
|
const editor = useEditor({
|
|
384
|
+
immediatelyRender: false,
|
|
284
385
|
extensions: [
|
|
285
386
|
StarterKit,
|
|
286
387
|
CommentMentionExtension,
|
|
287
|
-
Placeholder.configure({
|
|
288
|
-
placeholder
|
|
289
|
-
})
|
|
388
|
+
Placeholder.configure({ placeholder: () => placeholderRef.current })
|
|
290
389
|
],
|
|
291
|
-
content: value ?? "",
|
|
292
|
-
editable: !
|
|
293
|
-
editorProps: {
|
|
294
|
-
attributes: {
|
|
295
|
-
class: cx2(
|
|
296
|
-
"w-full bg-white border-2 rounded-md border-border-base",
|
|
297
|
-
"outline-none transition-all duration-300 ease-in-out",
|
|
298
|
-
"pl-5 py-3 font-normal text-base leading-relaxed",
|
|
299
|
-
"h-[120px] overflow-y-auto",
|
|
300
|
-
"focus:shadow-focus focus-visible:shadow-focus",
|
|
301
|
-
disabled && "bg-bg-subtle border-border-subtle text-fg-inactive"
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
},
|
|
390
|
+
content: richValue ?? stringContent(value ?? "", valueFormat),
|
|
391
|
+
editable: !locked,
|
|
392
|
+
editorProps: { attributes: editorAttributes },
|
|
305
393
|
onUpdate: ({ editor: editor2 }) => {
|
|
306
|
-
|
|
394
|
+
const next = readValue(editor2);
|
|
395
|
+
if (controlledRef.current && (callbacks.current.onChange || callbacks.current.onValueChange)) {
|
|
396
|
+
pendingEchoesRef.current.push(next);
|
|
397
|
+
if (pendingEchoesRef.current.length > 100)
|
|
398
|
+
pendingEchoesRef.current.shift();
|
|
399
|
+
}
|
|
400
|
+
setHasValue(!next.isEmpty);
|
|
401
|
+
callbacks.current.onChange?.({ target: { value: next.legacyText } });
|
|
402
|
+
callbacks.current.onValueChange?.(next);
|
|
307
403
|
}
|
|
308
404
|
});
|
|
309
|
-
|
|
405
|
+
useImperativeHandle(ref, () => containerRef.current, []);
|
|
406
|
+
useImperativeHandle(
|
|
407
|
+
composerRef,
|
|
408
|
+
() => ({
|
|
409
|
+
focus: (position = "end") => {
|
|
410
|
+
if (locked) return false;
|
|
411
|
+
if (!editor) {
|
|
412
|
+
pendingFocusRef.current = position;
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
return editor.commands.focus(position);
|
|
416
|
+
},
|
|
417
|
+
clear: () => !locked && Boolean(editor?.commands.clearContent(true)),
|
|
418
|
+
getValue: () => editor ? readValue(editor) : null,
|
|
419
|
+
submit: () => {
|
|
420
|
+
if (!locked) formRef.current?.requestSubmit();
|
|
421
|
+
}
|
|
422
|
+
}),
|
|
423
|
+
[editor, locked]
|
|
424
|
+
);
|
|
310
425
|
useEffect2(() => {
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
426
|
+
if (!editor) return;
|
|
427
|
+
editor.setEditable(!locked, false);
|
|
428
|
+
editor.setOptions({
|
|
429
|
+
editorProps: {
|
|
430
|
+
...editor.options.editorProps,
|
|
431
|
+
attributes: editorAttributes
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}, [editor, locked, editorAttributes, placeholder]);
|
|
317
435
|
useEffect2(() => {
|
|
318
436
|
if (!editor) return;
|
|
319
|
-
|
|
320
|
-
|
|
437
|
+
const next = richValue ?? value;
|
|
438
|
+
if (next === void 0) return;
|
|
439
|
+
const current = readValue(editor);
|
|
440
|
+
const echoIndex = pendingEchoesRef.current.findIndex(
|
|
441
|
+
(snapshot) => richValue !== void 0 ? JSON.stringify(snapshot.json) === JSON.stringify(richValue) : snapshot.text === value || snapshot.legacyText === value
|
|
442
|
+
);
|
|
443
|
+
if (echoIndex !== -1) {
|
|
444
|
+
pendingEchoesRef.current.splice(0, echoIndex + 1);
|
|
445
|
+
setHasValue(!current.isEmpty);
|
|
446
|
+
return;
|
|
321
447
|
}
|
|
322
|
-
|
|
448
|
+
pendingEchoesRef.current = [];
|
|
449
|
+
const equal = richValue !== void 0 ? JSON.stringify(current.json) === JSON.stringify(richValue) : current.text === value || current.legacyText === value;
|
|
450
|
+
if (!equal)
|
|
451
|
+
editor.commands.setContent(
|
|
452
|
+
richValue ?? stringContent(value, valueFormat),
|
|
453
|
+
{ emitUpdate: false }
|
|
454
|
+
);
|
|
455
|
+
setHasValue(!readValue(editor).isEmpty);
|
|
456
|
+
}, [editor, value, richValue, valueFormat]);
|
|
457
|
+
useEffect2(() => {
|
|
458
|
+
if (editor) setHasValue(!readValue(editor).isEmpty);
|
|
459
|
+
}, [editor]);
|
|
460
|
+
useEffect2(() => {
|
|
461
|
+
if (!editor || locked) return;
|
|
462
|
+
if (!autoFocus && pendingFocusRef.current === null) return;
|
|
463
|
+
const position = pendingFocusRef.current ?? "end";
|
|
464
|
+
pendingFocusRef.current = null;
|
|
465
|
+
const frame = requestAnimationFrame(() => {
|
|
466
|
+
if (!editor.isDestroyed && editor.isEditable)
|
|
467
|
+
editor.commands.focus(position);
|
|
468
|
+
});
|
|
469
|
+
return () => cancelAnimationFrame(frame);
|
|
470
|
+
}, [editor, autoFocus, locked]);
|
|
323
471
|
const contextValue = useMemo2(
|
|
324
472
|
() => ({
|
|
325
473
|
editor,
|
|
326
474
|
placeholder,
|
|
475
|
+
editable: !locked,
|
|
327
476
|
editorContainerRef: containerRef
|
|
328
477
|
}),
|
|
329
|
-
[editor, placeholder]
|
|
478
|
+
[editor, placeholder, locked]
|
|
330
479
|
);
|
|
331
480
|
return /* @__PURE__ */ jsx3(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
|
|
332
481
|
"form",
|
|
333
482
|
{
|
|
483
|
+
...props,
|
|
484
|
+
ref: formRef,
|
|
485
|
+
"data-comment-surface": "composer",
|
|
486
|
+
"aria-busy": isSubmitting || void 0,
|
|
334
487
|
className: cx2(
|
|
335
488
|
"relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
|
|
336
489
|
className
|
|
337
490
|
),
|
|
491
|
+
onKeyDown: (event) => {
|
|
492
|
+
if (!handledShortcutEvents.current.has(event.nativeEvent))
|
|
493
|
+
onKeyDown?.(event);
|
|
494
|
+
},
|
|
495
|
+
onKeyDownCapture: (event) => {
|
|
496
|
+
onKeyDownCapture?.(event);
|
|
497
|
+
if (!event.defaultPrevented && submitOnModEnter && !event.nativeEvent.isComposing && (event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
498
|
+
handledShortcutEvents.current.add(event.nativeEvent);
|
|
499
|
+
onKeyDown?.(event);
|
|
500
|
+
if (event.defaultPrevented) return;
|
|
501
|
+
event.preventDefault();
|
|
502
|
+
if (!locked && editor && !readValue(editor).isEmpty)
|
|
503
|
+
formRef.current?.requestSubmit();
|
|
504
|
+
}
|
|
505
|
+
},
|
|
338
506
|
onSubmit: (event) => {
|
|
339
507
|
event.preventDefault();
|
|
340
|
-
if (
|
|
341
|
-
|
|
508
|
+
if (!locked && editor && !readValue(editor).isEmpty)
|
|
509
|
+
onSubmit?.(event);
|
|
342
510
|
},
|
|
343
|
-
...props,
|
|
344
511
|
children: [
|
|
345
512
|
(avatar || avatarProps) && /* @__PURE__ */ jsx3("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx3(Avatar2, { size: "small", ...avatarProps }) }),
|
|
346
513
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -355,11 +522,11 @@ var CommentComposer = forwardRef2(function CommentComposer2({
|
|
|
355
522
|
size: "small",
|
|
356
523
|
isSecondary: true,
|
|
357
524
|
onClick: onCancel,
|
|
358
|
-
disabled:
|
|
525
|
+
disabled: locked,
|
|
359
526
|
children: cancelLabel
|
|
360
527
|
}
|
|
361
528
|
),
|
|
362
|
-
/* @__PURE__ */ jsx3(Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ?
|
|
529
|
+
!externalSubmit && /* @__PURE__ */ jsx3(Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? submittingLabel : submitLabel })
|
|
363
530
|
] })
|
|
364
531
|
] })
|
|
365
532
|
] }),
|
|
@@ -367,29 +534,97 @@ var CommentComposer = forwardRef2(function CommentComposer2({
|
|
|
367
534
|
]
|
|
368
535
|
}
|
|
369
536
|
) });
|
|
370
|
-
}
|
|
537
|
+
}
|
|
538
|
+
var CommentComposer = forwardRef2(CommentComposerComponent);
|
|
371
539
|
|
|
372
540
|
// src/elements/CommentMention.jsx
|
|
373
|
-
import React4, {
|
|
541
|
+
import React4, {
|
|
542
|
+
useMemo as useMemo3,
|
|
543
|
+
useState as useState4,
|
|
544
|
+
useCallback,
|
|
545
|
+
useEffect as useEffect3,
|
|
546
|
+
useRef as useRef2
|
|
547
|
+
} from "react";
|
|
374
548
|
import { Menu, MenuList, MenuItem } from "@edux-design/menus";
|
|
375
549
|
import { Avatar as Avatar3 } from "@edux-design/avatars";
|
|
550
|
+
|
|
551
|
+
// src/elements/useMentionPortalContainer.js
|
|
552
|
+
import { useLayoutEffect, useState as useState3 } from "react";
|
|
553
|
+
function resolveMentionPortalContainer(anchor, override) {
|
|
554
|
+
const document = anchor?.ownerDocument;
|
|
555
|
+
if (!document || !anchor.isConnected) return void 0;
|
|
556
|
+
if (override === null) return void 0;
|
|
557
|
+
if (override?.isConnected && override.ownerDocument === document)
|
|
558
|
+
return override;
|
|
559
|
+
const dialog = anchor.closest(
|
|
560
|
+
'[role="dialog"], [role="alertdialog"], dialog'
|
|
561
|
+
);
|
|
562
|
+
return dialog?.isConnected ? dialog : void 0;
|
|
563
|
+
}
|
|
564
|
+
function useMentionPortalContainer(anchorRef, override, active) {
|
|
565
|
+
const [container, setContainer] = useState3(void 0);
|
|
566
|
+
useLayoutEffect(() => {
|
|
567
|
+
let disposed = false;
|
|
568
|
+
let observer;
|
|
569
|
+
let observedDocument;
|
|
570
|
+
const refresh = () => {
|
|
571
|
+
if (disposed) return;
|
|
572
|
+
const anchor = anchorRef?.current;
|
|
573
|
+
const next = resolveMentionPortalContainer(anchor, override);
|
|
574
|
+
setContainer((previous) => previous === next ? previous : next);
|
|
575
|
+
const document = anchor?.ownerDocument;
|
|
576
|
+
if (!active || observedDocument === document) return;
|
|
577
|
+
observer?.disconnect();
|
|
578
|
+
observedDocument = document;
|
|
579
|
+
const MutationObserver = document?.defaultView?.MutationObserver;
|
|
580
|
+
if (MutationObserver && document.documentElement) {
|
|
581
|
+
observer = new MutationObserver(refresh);
|
|
582
|
+
observer.observe(document.documentElement, {
|
|
583
|
+
childList: true,
|
|
584
|
+
subtree: true,
|
|
585
|
+
attributes: true,
|
|
586
|
+
attributeFilter: ["role"]
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
refresh();
|
|
591
|
+
return () => {
|
|
592
|
+
disposed = true;
|
|
593
|
+
observer?.disconnect();
|
|
594
|
+
};
|
|
595
|
+
}, [anchorRef, override, active]);
|
|
596
|
+
return container;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/elements/CommentMention.jsx
|
|
376
600
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
377
601
|
var CommentMention = ({
|
|
378
602
|
peopleList = [],
|
|
379
|
-
onQueryChange
|
|
603
|
+
onQueryChange,
|
|
604
|
+
portalContainer
|
|
380
605
|
}) => {
|
|
381
|
-
const { editor, editorContainerRef } = useCommentComposer();
|
|
382
|
-
const [menuOpen, setMenuOpen] =
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
606
|
+
const { editor, editorContainerRef, editable = true } = useCommentComposer();
|
|
607
|
+
const [menuOpen, setMenuOpen] = useState4(false);
|
|
608
|
+
const mentionPortalContainer = useMentionPortalContainer(
|
|
609
|
+
editorContainerRef,
|
|
610
|
+
portalContainer,
|
|
611
|
+
menuOpen
|
|
612
|
+
);
|
|
613
|
+
const [query, setQuery] = useState4("");
|
|
614
|
+
const [anchorStyle, setAnchorStyle] = useState4({ left: 0, top: 0 });
|
|
386
615
|
const menuListRef = useRef2(null);
|
|
387
616
|
const rangeRef = useRef2(null);
|
|
388
617
|
const lastQueryRef = useRef2("");
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
)
|
|
618
|
+
const dismissedRangeRef = useRef2(null);
|
|
619
|
+
const focusFrameRef = useRef2(null);
|
|
620
|
+
const focusTimeoutRef = useRef2(null);
|
|
621
|
+
const dismissMenu = useCallback(() => {
|
|
622
|
+
const activeRange = rangeRef.current;
|
|
623
|
+
dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
|
|
624
|
+
setMenuOpen(false);
|
|
625
|
+
if (editor && !editor.isDestroyed && editor.isEditable !== false)
|
|
626
|
+
editor.view.focus();
|
|
627
|
+
}, [editor]);
|
|
393
628
|
const filteredPeople = useMemo3(() => {
|
|
394
629
|
if (!query) return peopleList;
|
|
395
630
|
const lowered = query.toLowerCase();
|
|
@@ -398,14 +633,15 @@ var CommentMention = ({
|
|
|
398
633
|
);
|
|
399
634
|
}, [peopleList, query]);
|
|
400
635
|
const updateMenuState = useCallback(() => {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
636
|
+
if (!editor || !editable || editor.isEditable === false) {
|
|
637
|
+
setMenuOpen(false);
|
|
638
|
+
rangeRef.current = null;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
404
641
|
const { parent, parentOffset, pos } = editor.state.selection.$from;
|
|
405
642
|
if (!parent.isTextblock) {
|
|
406
643
|
setMenuOpen(false);
|
|
407
644
|
setQuery("");
|
|
408
|
-
setRange(null);
|
|
409
645
|
return;
|
|
410
646
|
}
|
|
411
647
|
const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
|
|
@@ -417,13 +653,12 @@ var CommentMention = ({
|
|
|
417
653
|
onQueryChange(handleQuery);
|
|
418
654
|
}
|
|
419
655
|
setQuery(handleQuery);
|
|
420
|
-
setMenuOpen(true);
|
|
421
656
|
const fromPos = pos - match[0].length;
|
|
422
657
|
const nextRange = { from: fromPos, to: pos };
|
|
423
|
-
|
|
658
|
+
setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
|
|
424
659
|
rangeRef.current = nextRange;
|
|
425
660
|
const coords = editor.view.coordsAtPos(fromPos);
|
|
426
|
-
const containerRect =
|
|
661
|
+
const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
|
|
427
662
|
if (containerRect) {
|
|
428
663
|
setAnchorStyle({
|
|
429
664
|
left: coords.left - containerRect.left + 12,
|
|
@@ -434,16 +669,16 @@ var CommentMention = ({
|
|
|
434
669
|
}
|
|
435
670
|
setMenuOpen(false);
|
|
436
671
|
setQuery("");
|
|
437
|
-
setRange(null);
|
|
438
672
|
rangeRef.current = null;
|
|
439
673
|
if (onQueryChange && lastQueryRef.current !== "") {
|
|
440
674
|
lastQueryRef.current = "";
|
|
441
675
|
onQueryChange("");
|
|
442
676
|
}
|
|
443
|
-
}, [editor, editorContainerRef, onQueryChange]);
|
|
677
|
+
}, [editor, editorContainerRef, onQueryChange, editable]);
|
|
444
678
|
const handleSelectPerson = useCallback(
|
|
445
679
|
(person) => {
|
|
446
|
-
if (!editor || !rangeRef.current)
|
|
680
|
+
if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
|
|
681
|
+
return;
|
|
447
682
|
const selectionRange = rangeRef.current;
|
|
448
683
|
editor.commands.focus();
|
|
449
684
|
editor.commands.setTextSelection(selectionRange);
|
|
@@ -463,21 +698,34 @@ var CommentMention = ({
|
|
|
463
698
|
{ updateSelection: false }
|
|
464
699
|
);
|
|
465
700
|
const nextPos = selectionRange.from + 2;
|
|
466
|
-
|
|
701
|
+
editor.view.focus();
|
|
702
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
703
|
+
clearTimeout(focusTimeoutRef.current);
|
|
704
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
705
|
+
if (editor.isDestroyed || editor.isEditable === false) return;
|
|
467
706
|
editor.commands.setTextSelection(nextPos);
|
|
468
707
|
editor.view.focus();
|
|
469
|
-
setTimeout(() =>
|
|
708
|
+
focusTimeoutRef.current = setTimeout(() => {
|
|
709
|
+
if (!editor.isDestroyed && editor.isEditable !== false)
|
|
710
|
+
editor.view.focus();
|
|
711
|
+
}, 0);
|
|
470
712
|
});
|
|
471
713
|
setMenuOpen(false);
|
|
472
714
|
setQuery("");
|
|
473
|
-
setRange(null);
|
|
474
715
|
rangeRef.current = null;
|
|
475
716
|
if (onQueryChange && lastQueryRef.current !== "") {
|
|
476
717
|
lastQueryRef.current = "";
|
|
477
718
|
onQueryChange("");
|
|
478
719
|
}
|
|
479
720
|
},
|
|
480
|
-
[editor, onQueryChange]
|
|
721
|
+
[editor, onQueryChange, editable]
|
|
722
|
+
);
|
|
723
|
+
useEffect3(
|
|
724
|
+
() => () => {
|
|
725
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
726
|
+
clearTimeout(focusTimeoutRef.current);
|
|
727
|
+
},
|
|
728
|
+
[editor]
|
|
481
729
|
);
|
|
482
730
|
useEffect3(() => {
|
|
483
731
|
if (!editor) return;
|
|
@@ -490,25 +738,30 @@ var CommentMention = ({
|
|
|
490
738
|
};
|
|
491
739
|
}, [editor, updateMenuState]);
|
|
492
740
|
useEffect3(() => {
|
|
493
|
-
var _a;
|
|
494
741
|
if (!editor) return;
|
|
495
742
|
const handleKeyDown = (event) => {
|
|
496
|
-
var _a2;
|
|
497
743
|
if (!menuOpen) return;
|
|
744
|
+
if (event.key === "Escape") {
|
|
745
|
+
event.preventDefault();
|
|
746
|
+
event.stopPropagation();
|
|
747
|
+
dismissMenu();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
498
750
|
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
499
751
|
event.preventDefault();
|
|
752
|
+
event.stopPropagation();
|
|
500
753
|
const list = menuListRef.current;
|
|
501
|
-
const items = list
|
|
502
|
-
if (!list || !
|
|
754
|
+
const items = list?.querySelectorAll("[data-menu-item]");
|
|
755
|
+
if (!list || !items?.length) return;
|
|
503
756
|
list.focus();
|
|
504
757
|
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
505
758
|
const targetIndex = direction === 1 ? 0 : items.length - 1;
|
|
506
|
-
|
|
759
|
+
items[targetIndex]?.focus();
|
|
507
760
|
};
|
|
508
|
-
const dom =
|
|
509
|
-
dom
|
|
510
|
-
return () => dom
|
|
511
|
-
}, [editor, menuOpen]);
|
|
761
|
+
const dom = editor.view?.dom;
|
|
762
|
+
dom?.addEventListener("keydown", handleKeyDown, true);
|
|
763
|
+
return () => dom?.removeEventListener("keydown", handleKeyDown, true);
|
|
764
|
+
}, [editor, menuOpen, dismissMenu]);
|
|
512
765
|
const menuContent = useMemo3(
|
|
513
766
|
() => /* @__PURE__ */ jsx4(
|
|
514
767
|
Menu,
|
|
@@ -520,6 +773,7 @@ var CommentMention = ({
|
|
|
520
773
|
{
|
|
521
774
|
type: "button",
|
|
522
775
|
"aria-hidden": true,
|
|
776
|
+
tabIndex: -1,
|
|
523
777
|
className: "absolute h-1 w-1 opacity-0",
|
|
524
778
|
style: {
|
|
525
779
|
left: `${anchorStyle.left}px`,
|
|
@@ -537,34 +791,56 @@ var CommentMention = ({
|
|
|
537
791
|
sideOffset: 4,
|
|
538
792
|
avoidCollisions: true,
|
|
539
793
|
collisionPadding: 12,
|
|
794
|
+
container: mentionPortalContainer,
|
|
540
795
|
onCloseAutoFocus: (event) => event.preventDefault()
|
|
541
796
|
},
|
|
542
|
-
children: /* @__PURE__ */ jsx4(
|
|
543
|
-
|
|
797
|
+
children: /* @__PURE__ */ jsx4(
|
|
798
|
+
MenuList,
|
|
544
799
|
{
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
800
|
+
ref: menuListRef,
|
|
801
|
+
className: "min-w-[220px]",
|
|
802
|
+
onKeyDownCapture: (event) => {
|
|
803
|
+
if (event.key === "Escape") {
|
|
804
|
+
event.preventDefault();
|
|
805
|
+
event.stopPropagation();
|
|
806
|
+
dismissMenu();
|
|
807
|
+
}
|
|
549
808
|
},
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
809
|
+
children: filteredPeople.map((person) => /* @__PURE__ */ jsx4(
|
|
810
|
+
MenuItem,
|
|
811
|
+
{
|
|
812
|
+
onMouseDown: (event) => {
|
|
813
|
+
event.preventDefault();
|
|
814
|
+
event.stopPropagation();
|
|
815
|
+
handleSelectPerson(person);
|
|
816
|
+
},
|
|
817
|
+
onClick: () => handleSelectPerson(person),
|
|
818
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
819
|
+
children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-8", children: [
|
|
820
|
+
/* @__PURE__ */ jsx4(Avatar3, { size: "small", ...person.avatarProps }),
|
|
821
|
+
/* @__PURE__ */ jsxs4("span", { className: "flex flex-col", children: [
|
|
822
|
+
/* @__PURE__ */ jsx4("span", { className: "text-sm text-fg-base", children: person.name }),
|
|
823
|
+
/* @__PURE__ */ jsxs4("span", { className: "text-xs text-fg-subtle", children: [
|
|
824
|
+
"@",
|
|
825
|
+
person.handle
|
|
826
|
+
] })
|
|
827
|
+
] })
|
|
559
828
|
] })
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
)
|
|
829
|
+
},
|
|
830
|
+
person.id
|
|
831
|
+
))
|
|
832
|
+
}
|
|
833
|
+
)
|
|
565
834
|
}
|
|
566
835
|
),
|
|
567
|
-
[
|
|
836
|
+
[
|
|
837
|
+
filteredPeople,
|
|
838
|
+
handleSelectPerson,
|
|
839
|
+
menuOpen,
|
|
840
|
+
anchorStyle,
|
|
841
|
+
dismissMenu,
|
|
842
|
+
mentionPortalContainer
|
|
843
|
+
]
|
|
568
844
|
);
|
|
569
845
|
if (!editor) return null;
|
|
570
846
|
return menuContent;
|