@edux-design/comments 0.1.3 → 0.2.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 +19 -0
- package/README.md +64 -4
- package/dist/index.d.mts +162 -47
- package/dist/index.d.ts +162 -47
- package/dist/index.js +370 -149
- package/dist/index.mjs +383 -154
- package/docs/composer.md +65 -0
- package/package.json +12 -23
- package/src/demos/Comment.stories.jsx +0 -472
- package/src/demos/CommentComposer.stories.jsx +0 -62
- package/src/elements/Comment.jsx +0 -234
- package/src/elements/Comment.test.jsx +0 -137
- package/src/elements/CommentComposer.jsx +0 -184
- package/src/elements/CommentComposer.test.jsx +0 -125
- package/src/elements/CommentMention.jsx +0 -217
- package/src/elements/CommentMention.test.jsx +0 -138
- package/src/elements/CommentMentionExtension.jsx +0 -56
- package/src/index.js +0 -3
- package/tsconfig.json +0 -10
package/dist/index.mjs
CHANGED
|
@@ -17,10 +17,14 @@ var VARIANT_STYLES = {
|
|
|
17
17
|
};
|
|
18
18
|
var CommentContext = createContext({
|
|
19
19
|
editable: true,
|
|
20
|
-
onResolve: void 0
|
|
20
|
+
onResolve: void 0,
|
|
21
|
+
resolved: false
|
|
21
22
|
});
|
|
22
23
|
var useCommentContext = () => useContext(CommentContext);
|
|
23
|
-
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({
|
|
24
28
|
author = "Author",
|
|
25
29
|
timestamp,
|
|
26
30
|
avatar,
|
|
@@ -38,10 +42,7 @@ var Comment = forwardRef(function Comment2({
|
|
|
38
42
|
}, ref) {
|
|
39
43
|
const childArray = React.Children.toArray(children);
|
|
40
44
|
const actionsSlot = childArray.find(
|
|
41
|
-
(child) =>
|
|
42
|
-
var _a;
|
|
43
|
-
return isValidElement(child) && (child.type === CommentActions || ((_a = child.type) == null ? void 0 : _a.displayName) === "Comment.Actions");
|
|
44
|
-
}
|
|
45
|
+
(child) => isValidElement(child) && (child.type === CommentActions || child.type?.displayName === "Comment.Actions")
|
|
45
46
|
);
|
|
46
47
|
const bodyNodes = childArray.filter((child) => child !== actionsSlot);
|
|
47
48
|
const bodyContent = body ?? bodyNodes;
|
|
@@ -49,9 +50,11 @@ var Comment = forwardRef(function Comment2({
|
|
|
49
50
|
const isResolved = resolved || variant === "resolved";
|
|
50
51
|
const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
|
|
51
52
|
const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
|
|
52
|
-
const shouldRenderAvatar = Boolean(
|
|
53
|
+
const shouldRenderAvatar = Boolean(
|
|
54
|
+
avatar || avatarProps || resolvedAvatarName
|
|
55
|
+
);
|
|
53
56
|
const replyItems = useMemo(
|
|
54
|
-
() =>
|
|
57
|
+
() => flattenReplies(replies).filter(
|
|
55
58
|
(reply) => reply !== null && reply !== void 0 && reply !== false
|
|
56
59
|
),
|
|
57
60
|
[replies]
|
|
@@ -63,49 +66,55 @@ var Comment = forwardRef(function Comment2({
|
|
|
63
66
|
setShowAllReplies(!shouldCollapseReplies);
|
|
64
67
|
}, [shouldCollapseReplies]);
|
|
65
68
|
const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
|
|
66
|
-
return /* @__PURE__ */ jsx(
|
|
67
|
-
|
|
69
|
+
return /* @__PURE__ */ jsx(
|
|
70
|
+
CommentContext.Provider,
|
|
68
71
|
{
|
|
69
|
-
|
|
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
|
-
|
|
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
|
+
)
|
|
106
115
|
}
|
|
107
|
-
)
|
|
108
|
-
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
109
118
|
var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ jsx(
|
|
110
119
|
"div",
|
|
111
120
|
{
|
|
@@ -139,31 +148,39 @@ var CommentResolve = ({
|
|
|
139
148
|
className,
|
|
140
149
|
disabled,
|
|
141
150
|
onClick,
|
|
151
|
+
label,
|
|
142
152
|
...props
|
|
143
153
|
}) => {
|
|
144
|
-
const { editable, onResolve } = useCommentContext();
|
|
154
|
+
const { editable, onResolve, resolved } = useCommentContext();
|
|
145
155
|
const isDisabled = disabled || !editable;
|
|
146
156
|
const handleClick = onClick ?? onResolve;
|
|
157
|
+
const isResolved = Boolean(resolved);
|
|
158
|
+
const actionLabel = label ?? (isResolved ? "Re-open" : "Resolve");
|
|
159
|
+
const toneClass = isResolved ? "text-fg-info-base" : "text-fg-success-base";
|
|
147
160
|
return /* @__PURE__ */ jsx(
|
|
148
161
|
"button",
|
|
149
162
|
{
|
|
150
163
|
type: "button",
|
|
151
164
|
className: cx(
|
|
152
|
-
"text-xs font-semibold
|
|
165
|
+
"text-xs font-semibold cursor-pointer",
|
|
166
|
+
toneClass,
|
|
153
167
|
className
|
|
154
168
|
),
|
|
155
169
|
onClick: handleClick,
|
|
156
170
|
disabled: isDisabled,
|
|
157
171
|
"aria-disabled": isDisabled,
|
|
158
172
|
...props,
|
|
159
|
-
children:
|
|
173
|
+
children: actionLabel
|
|
160
174
|
}
|
|
161
175
|
);
|
|
162
176
|
};
|
|
163
|
-
Comment
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
var Comment = Object.assign(forwardRef(CommentComponent), {
|
|
178
|
+
Actions: CommentActions,
|
|
179
|
+
Reply: CommentReply,
|
|
180
|
+
Edit: CommentEdit,
|
|
181
|
+
Resolve: CommentResolve
|
|
182
|
+
});
|
|
183
|
+
Comment.displayName = "Comment";
|
|
167
184
|
CommentActions.displayName = "Comment.Actions";
|
|
168
185
|
CommentReply.displayName = "Comment.Reply";
|
|
169
186
|
CommentEdit.displayName = "Comment.Edit";
|
|
@@ -176,7 +193,9 @@ import React3, {
|
|
|
176
193
|
useRef,
|
|
177
194
|
createContext as createContext2,
|
|
178
195
|
useContext as useContext2,
|
|
179
|
-
useEffect as useEffect2
|
|
196
|
+
useEffect as useEffect2,
|
|
197
|
+
useImperativeHandle,
|
|
198
|
+
useState as useState2
|
|
180
199
|
} from "react";
|
|
181
200
|
import { cx as cx2 } from "@edux-design/utils";
|
|
182
201
|
import { Avatar as Avatar2 } from "@edux-design/avatars";
|
|
@@ -192,9 +211,8 @@ import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
|
|
|
192
211
|
import { Mention } from "@edux-design/mentions";
|
|
193
212
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
194
213
|
var MentionNodeView = ({ node }) => {
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
|
|
214
|
+
const handle = node.attrs?.handle ?? "";
|
|
215
|
+
const label = node.attrs?.label ?? "";
|
|
198
216
|
return /* @__PURE__ */ jsx2(
|
|
199
217
|
NodeViewWrapper,
|
|
200
218
|
{
|
|
@@ -237,6 +255,11 @@ var CommentMentionExtension = Node.create({
|
|
|
237
255
|
label
|
|
238
256
|
];
|
|
239
257
|
},
|
|
258
|
+
renderText({ node }) {
|
|
259
|
+
const handle = node.attrs.handle || "";
|
|
260
|
+
const label = node.attrs.label || "";
|
|
261
|
+
return `@${handle || label || "mention"}`;
|
|
262
|
+
},
|
|
240
263
|
addNodeView() {
|
|
241
264
|
return ReactNodeViewRenderer(MentionNodeView);
|
|
242
265
|
}
|
|
@@ -254,87 +277,237 @@ var useCommentComposer = () => {
|
|
|
254
277
|
}
|
|
255
278
|
return context;
|
|
256
279
|
};
|
|
257
|
-
|
|
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({
|
|
258
321
|
value,
|
|
322
|
+
valueFormat = "html",
|
|
323
|
+
richValue,
|
|
259
324
|
onChange,
|
|
325
|
+
onValueChange,
|
|
260
326
|
onSubmit,
|
|
261
327
|
onCancel,
|
|
328
|
+
composerRef,
|
|
329
|
+
editorLabel = "Comment",
|
|
330
|
+
editorAttributes: suppliedEditorAttributes,
|
|
331
|
+
autoFocus = false,
|
|
332
|
+
externalSubmit = false,
|
|
333
|
+
submitOnModEnter = false,
|
|
262
334
|
placeholder = "Write a comment...",
|
|
263
335
|
rows = 4,
|
|
264
336
|
disabled = false,
|
|
265
337
|
isSubmitting = false,
|
|
266
338
|
submitLabel = "Post",
|
|
339
|
+
submittingLabel = "Posting...",
|
|
267
340
|
cancelLabel = "Cancel",
|
|
268
341
|
avatar,
|
|
269
342
|
avatarProps,
|
|
270
343
|
actions,
|
|
271
344
|
className,
|
|
272
345
|
children,
|
|
346
|
+
onKeyDown,
|
|
347
|
+
onKeyDownCapture,
|
|
273
348
|
...props
|
|
274
349
|
}, ref) {
|
|
275
|
-
const
|
|
276
|
-
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;
|
|
277
383
|
const editor = useEditor({
|
|
384
|
+
immediatelyRender: false,
|
|
278
385
|
extensions: [
|
|
279
386
|
StarterKit,
|
|
280
387
|
CommentMentionExtension,
|
|
281
|
-
Placeholder.configure({
|
|
282
|
-
placeholder
|
|
283
|
-
})
|
|
388
|
+
Placeholder.configure({ placeholder: () => placeholderRef.current })
|
|
284
389
|
],
|
|
285
|
-
content: value ?? "",
|
|
286
|
-
editable: !
|
|
287
|
-
editorProps: {
|
|
288
|
-
attributes: {
|
|
289
|
-
class: cx2(
|
|
290
|
-
"w-full bg-white border-2 rounded-md border-border-base",
|
|
291
|
-
"outline-none transition-all duration-300 ease-in-out",
|
|
292
|
-
"pl-5 py-3 font-normal text-base leading-relaxed",
|
|
293
|
-
"h-[120px] overflow-y-auto",
|
|
294
|
-
"focus:shadow-focus focus-visible:shadow-focus",
|
|
295
|
-
disabled && "bg-bg-subtle border-border-subtle text-fg-inactive"
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
},
|
|
390
|
+
content: richValue ?? stringContent(value ?? "", valueFormat),
|
|
391
|
+
editable: !locked,
|
|
392
|
+
editorProps: { attributes: editorAttributes },
|
|
299
393
|
onUpdate: ({ editor: editor2 }) => {
|
|
300
|
-
|
|
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);
|
|
301
403
|
}
|
|
302
404
|
});
|
|
303
|
-
|
|
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
|
+
);
|
|
304
425
|
useEffect2(() => {
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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]);
|
|
311
435
|
useEffect2(() => {
|
|
312
436
|
if (!editor) return;
|
|
313
|
-
|
|
314
|
-
|
|
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;
|
|
315
447
|
}
|
|
316
|
-
|
|
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]);
|
|
317
471
|
const contextValue = useMemo2(
|
|
318
472
|
() => ({
|
|
319
473
|
editor,
|
|
320
474
|
placeholder,
|
|
475
|
+
editable: !locked,
|
|
321
476
|
editorContainerRef: containerRef
|
|
322
477
|
}),
|
|
323
|
-
[editor, placeholder]
|
|
478
|
+
[editor, placeholder, locked]
|
|
324
479
|
);
|
|
325
480
|
return /* @__PURE__ */ jsx3(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
|
|
326
481
|
"form",
|
|
327
482
|
{
|
|
483
|
+
...props,
|
|
484
|
+
ref: formRef,
|
|
485
|
+
"data-comment-surface": "composer",
|
|
486
|
+
"aria-busy": isSubmitting || void 0,
|
|
328
487
|
className: cx2(
|
|
329
488
|
"relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
|
|
330
489
|
className
|
|
331
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
|
+
},
|
|
332
506
|
onSubmit: (event) => {
|
|
333
507
|
event.preventDefault();
|
|
334
|
-
if (
|
|
335
|
-
|
|
508
|
+
if (!locked && editor && !readValue(editor).isEmpty)
|
|
509
|
+
onSubmit?.(event);
|
|
336
510
|
},
|
|
337
|
-
...props,
|
|
338
511
|
children: [
|
|
339
512
|
(avatar || avatarProps) && /* @__PURE__ */ jsx3("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx3(Avatar2, { size: "small", ...avatarProps }) }),
|
|
340
513
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -349,11 +522,11 @@ var CommentComposer = forwardRef2(function CommentComposer2({
|
|
|
349
522
|
size: "small",
|
|
350
523
|
isSecondary: true,
|
|
351
524
|
onClick: onCancel,
|
|
352
|
-
disabled:
|
|
525
|
+
disabled: locked,
|
|
353
526
|
children: cancelLabel
|
|
354
527
|
}
|
|
355
528
|
),
|
|
356
|
-
/* @__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 })
|
|
357
530
|
] })
|
|
358
531
|
] })
|
|
359
532
|
] }),
|
|
@@ -361,27 +534,38 @@ var CommentComposer = forwardRef2(function CommentComposer2({
|
|
|
361
534
|
]
|
|
362
535
|
}
|
|
363
536
|
) });
|
|
364
|
-
}
|
|
537
|
+
}
|
|
538
|
+
var CommentComposer = forwardRef2(CommentComposerComponent);
|
|
365
539
|
|
|
366
540
|
// src/elements/CommentMention.jsx
|
|
367
|
-
import React4, {
|
|
541
|
+
import React4, {
|
|
542
|
+
useMemo as useMemo3,
|
|
543
|
+
useState as useState3,
|
|
544
|
+
useCallback,
|
|
545
|
+
useEffect as useEffect3,
|
|
546
|
+
useRef as useRef2
|
|
547
|
+
} from "react";
|
|
368
548
|
import { Menu, MenuList, MenuItem } from "@edux-design/menus";
|
|
369
549
|
import { Avatar as Avatar3 } from "@edux-design/avatars";
|
|
370
550
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
371
|
-
var CommentMention = ({
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
const [
|
|
376
|
-
const [query, setQuery] = useState2("");
|
|
377
|
-
const [range, setRange] = useState2(null);
|
|
378
|
-
const [anchorStyle, setAnchorStyle] = useState2({ left: 0, top: 0 });
|
|
551
|
+
var CommentMention = ({ peopleList = [], onQueryChange }) => {
|
|
552
|
+
const { editor, editorContainerRef, editable = true } = useCommentComposer();
|
|
553
|
+
const [menuOpen, setMenuOpen] = useState3(false);
|
|
554
|
+
const [query, setQuery] = useState3("");
|
|
555
|
+
const [anchorStyle, setAnchorStyle] = useState3({ left: 0, top: 0 });
|
|
379
556
|
const menuListRef = useRef2(null);
|
|
380
557
|
const rangeRef = useRef2(null);
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
);
|
|
558
|
+
const lastQueryRef = useRef2("");
|
|
559
|
+
const dismissedRangeRef = useRef2(null);
|
|
560
|
+
const focusFrameRef = useRef2(null);
|
|
561
|
+
const focusTimeoutRef = useRef2(null);
|
|
562
|
+
const dismissMenu = useCallback(() => {
|
|
563
|
+
const activeRange = rangeRef.current;
|
|
564
|
+
dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
|
|
565
|
+
setMenuOpen(false);
|
|
566
|
+
if (editor && !editor.isDestroyed && editor.isEditable !== false)
|
|
567
|
+
editor.view.focus();
|
|
568
|
+
}, [editor]);
|
|
385
569
|
const filteredPeople = useMemo3(() => {
|
|
386
570
|
if (!query) return peopleList;
|
|
387
571
|
const lowered = query.toLowerCase();
|
|
@@ -390,28 +574,32 @@ var CommentMention = ({
|
|
|
390
574
|
);
|
|
391
575
|
}, [peopleList, query]);
|
|
392
576
|
const updateMenuState = useCallback(() => {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
577
|
+
if (!editor || !editable || editor.isEditable === false) {
|
|
578
|
+
setMenuOpen(false);
|
|
579
|
+
rangeRef.current = null;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
396
582
|
const { parent, parentOffset, pos } = editor.state.selection.$from;
|
|
397
583
|
if (!parent.isTextblock) {
|
|
398
584
|
setMenuOpen(false);
|
|
399
585
|
setQuery("");
|
|
400
|
-
setRange(null);
|
|
401
586
|
return;
|
|
402
587
|
}
|
|
403
588
|
const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
|
|
404
589
|
const match = textBefore.match(/@([\w-]*)$/);
|
|
405
590
|
if (match) {
|
|
406
591
|
const handleQuery = match[1] ?? "";
|
|
592
|
+
if (onQueryChange && handleQuery !== lastQueryRef.current) {
|
|
593
|
+
lastQueryRef.current = handleQuery;
|
|
594
|
+
onQueryChange(handleQuery);
|
|
595
|
+
}
|
|
407
596
|
setQuery(handleQuery);
|
|
408
|
-
setMenuOpen(true);
|
|
409
597
|
const fromPos = pos - match[0].length;
|
|
410
598
|
const nextRange = { from: fromPos, to: pos };
|
|
411
|
-
|
|
599
|
+
setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
|
|
412
600
|
rangeRef.current = nextRange;
|
|
413
601
|
const coords = editor.view.coordsAtPos(fromPos);
|
|
414
|
-
const containerRect =
|
|
602
|
+
const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
|
|
415
603
|
if (containerRect) {
|
|
416
604
|
setAnchorStyle({
|
|
417
605
|
left: coords.left - containerRect.left + 12,
|
|
@@ -422,12 +610,16 @@ var CommentMention = ({
|
|
|
422
610
|
}
|
|
423
611
|
setMenuOpen(false);
|
|
424
612
|
setQuery("");
|
|
425
|
-
setRange(null);
|
|
426
613
|
rangeRef.current = null;
|
|
427
|
-
|
|
614
|
+
if (onQueryChange && lastQueryRef.current !== "") {
|
|
615
|
+
lastQueryRef.current = "";
|
|
616
|
+
onQueryChange("");
|
|
617
|
+
}
|
|
618
|
+
}, [editor, editorContainerRef, onQueryChange, editable]);
|
|
428
619
|
const handleSelectPerson = useCallback(
|
|
429
620
|
(person) => {
|
|
430
|
-
if (!editor || !rangeRef.current)
|
|
621
|
+
if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
|
|
622
|
+
return;
|
|
431
623
|
const selectionRange = rangeRef.current;
|
|
432
624
|
editor.commands.focus();
|
|
433
625
|
editor.commands.setTextSelection(selectionRange);
|
|
@@ -447,15 +639,32 @@ var CommentMention = ({
|
|
|
447
639
|
{ updateSelection: false }
|
|
448
640
|
);
|
|
449
641
|
const nextPos = selectionRange.from + 2;
|
|
450
|
-
|
|
642
|
+
editor.view.focus();
|
|
643
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
644
|
+
clearTimeout(focusTimeoutRef.current);
|
|
645
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
646
|
+
if (editor.isDestroyed || editor.isEditable === false) return;
|
|
451
647
|
editor.commands.setTextSelection(nextPos);
|
|
452
648
|
editor.view.focus();
|
|
453
|
-
setTimeout(() =>
|
|
649
|
+
focusTimeoutRef.current = setTimeout(() => {
|
|
650
|
+
if (!editor.isDestroyed && editor.isEditable !== false)
|
|
651
|
+
editor.view.focus();
|
|
652
|
+
}, 0);
|
|
454
653
|
});
|
|
455
654
|
setMenuOpen(false);
|
|
456
655
|
setQuery("");
|
|
457
|
-
setRange(null);
|
|
458
656
|
rangeRef.current = null;
|
|
657
|
+
if (onQueryChange && lastQueryRef.current !== "") {
|
|
658
|
+
lastQueryRef.current = "";
|
|
659
|
+
onQueryChange("");
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
[editor, onQueryChange, editable]
|
|
663
|
+
);
|
|
664
|
+
useEffect3(
|
|
665
|
+
() => () => {
|
|
666
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
667
|
+
clearTimeout(focusTimeoutRef.current);
|
|
459
668
|
},
|
|
460
669
|
[editor]
|
|
461
670
|
);
|
|
@@ -470,25 +679,30 @@ var CommentMention = ({
|
|
|
470
679
|
};
|
|
471
680
|
}, [editor, updateMenuState]);
|
|
472
681
|
useEffect3(() => {
|
|
473
|
-
var _a;
|
|
474
682
|
if (!editor) return;
|
|
475
683
|
const handleKeyDown = (event) => {
|
|
476
|
-
var _a2;
|
|
477
684
|
if (!menuOpen) return;
|
|
685
|
+
if (event.key === "Escape") {
|
|
686
|
+
event.preventDefault();
|
|
687
|
+
event.stopPropagation();
|
|
688
|
+
dismissMenu();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
478
691
|
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
479
692
|
event.preventDefault();
|
|
693
|
+
event.stopPropagation();
|
|
480
694
|
const list = menuListRef.current;
|
|
481
|
-
const items = list
|
|
482
|
-
if (!list || !
|
|
695
|
+
const items = list?.querySelectorAll("[data-menu-item]");
|
|
696
|
+
if (!list || !items?.length) return;
|
|
483
697
|
list.focus();
|
|
484
698
|
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
485
699
|
const targetIndex = direction === 1 ? 0 : items.length - 1;
|
|
486
|
-
|
|
700
|
+
items[targetIndex]?.focus();
|
|
487
701
|
};
|
|
488
|
-
const dom =
|
|
489
|
-
dom
|
|
490
|
-
return () => dom
|
|
491
|
-
}, [editor, menuOpen]);
|
|
702
|
+
const dom = editor.view?.dom;
|
|
703
|
+
dom?.addEventListener("keydown", handleKeyDown, true);
|
|
704
|
+
return () => dom?.removeEventListener("keydown", handleKeyDown, true);
|
|
705
|
+
}, [editor, menuOpen, dismissMenu]);
|
|
492
706
|
const menuContent = useMemo3(
|
|
493
707
|
() => /* @__PURE__ */ jsx4(
|
|
494
708
|
Menu,
|
|
@@ -500,6 +714,7 @@ var CommentMention = ({
|
|
|
500
714
|
{
|
|
501
715
|
type: "button",
|
|
502
716
|
"aria-hidden": true,
|
|
717
|
+
tabIndex: -1,
|
|
503
718
|
className: "absolute h-1 w-1 opacity-0",
|
|
504
719
|
style: {
|
|
505
720
|
left: `${anchorStyle.left}px`,
|
|
@@ -519,32 +734,46 @@ var CommentMention = ({
|
|
|
519
734
|
collisionPadding: 12,
|
|
520
735
|
onCloseAutoFocus: (event) => event.preventDefault()
|
|
521
736
|
},
|
|
522
|
-
children: /* @__PURE__ */ jsx4(
|
|
523
|
-
|
|
737
|
+
children: /* @__PURE__ */ jsx4(
|
|
738
|
+
MenuList,
|
|
524
739
|
{
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
740
|
+
ref: menuListRef,
|
|
741
|
+
className: "min-w-[220px]",
|
|
742
|
+
onKeyDownCapture: (event) => {
|
|
743
|
+
if (event.key === "Escape") {
|
|
744
|
+
event.preventDefault();
|
|
745
|
+
event.stopPropagation();
|
|
746
|
+
dismissMenu();
|
|
747
|
+
}
|
|
529
748
|
},
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
749
|
+
children: filteredPeople.map((person) => /* @__PURE__ */ jsx4(
|
|
750
|
+
MenuItem,
|
|
751
|
+
{
|
|
752
|
+
onMouseDown: (event) => {
|
|
753
|
+
event.preventDefault();
|
|
754
|
+
event.stopPropagation();
|
|
755
|
+
handleSelectPerson(person);
|
|
756
|
+
},
|
|
757
|
+
onClick: () => handleSelectPerson(person),
|
|
758
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
759
|
+
children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-8", children: [
|
|
760
|
+
/* @__PURE__ */ jsx4(Avatar3, { size: "small", ...person.avatarProps }),
|
|
761
|
+
/* @__PURE__ */ jsxs4("span", { className: "flex flex-col", children: [
|
|
762
|
+
/* @__PURE__ */ jsx4("span", { className: "text-sm text-fg-base", children: person.name }),
|
|
763
|
+
/* @__PURE__ */ jsxs4("span", { className: "text-xs text-fg-subtle", children: [
|
|
764
|
+
"@",
|
|
765
|
+
person.handle
|
|
766
|
+
] })
|
|
767
|
+
] })
|
|
539
768
|
] })
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
769
|
+
},
|
|
770
|
+
person.id
|
|
771
|
+
))
|
|
772
|
+
}
|
|
773
|
+
)
|
|
545
774
|
}
|
|
546
775
|
),
|
|
547
|
-
[filteredPeople, handleSelectPerson, menuOpen]
|
|
776
|
+
[filteredPeople, handleSelectPerson, menuOpen, anchorStyle, dismissMenu]
|
|
548
777
|
);
|
|
549
778
|
if (!editor) return null;
|
|
550
779
|
return menuContent;
|