@edux-design/comments 0.1.4 → 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 +13 -0
- package/README.md +18 -0
- package/dist/index.d.mts +162 -48
- package/dist/index.d.ts +162 -48
- package/dist/index.js +348 -147
- package/dist/index.mjs +361 -152
- package/docs/composer.md +65 -0
- package/package.json +12 -23
- 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.js
CHANGED
|
@@ -50,7 +50,10 @@ var CommentContext = (0, import_react.createContext)({
|
|
|
50
50
|
resolved: false
|
|
51
51
|
});
|
|
52
52
|
var useCommentContext = () => (0, import_react.useContext)(CommentContext);
|
|
53
|
-
var
|
|
53
|
+
var flattenReplies = (children) => import_react.default.Children.toArray(children).flatMap(
|
|
54
|
+
(child) => (0, import_react.isValidElement)(child) && child.type === import_react.default.Fragment ? flattenReplies(child.props.children) : [child]
|
|
55
|
+
);
|
|
56
|
+
function CommentComponent({
|
|
54
57
|
author = "Author",
|
|
55
58
|
timestamp,
|
|
56
59
|
avatar,
|
|
@@ -68,10 +71,7 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
|
|
|
68
71
|
}, ref) {
|
|
69
72
|
const childArray = import_react.default.Children.toArray(children);
|
|
70
73
|
const actionsSlot = childArray.find(
|
|
71
|
-
(child) =>
|
|
72
|
-
var _a;
|
|
73
|
-
return (0, import_react.isValidElement)(child) && (child.type === CommentActions || ((_a = child.type) == null ? void 0 : _a.displayName) === "Comment.Actions");
|
|
74
|
-
}
|
|
74
|
+
(child) => (0, import_react.isValidElement)(child) && (child.type === CommentActions || child.type?.displayName === "Comment.Actions")
|
|
75
75
|
);
|
|
76
76
|
const bodyNodes = childArray.filter((child) => child !== actionsSlot);
|
|
77
77
|
const bodyContent = body ?? bodyNodes;
|
|
@@ -79,9 +79,11 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
|
|
|
79
79
|
const isResolved = resolved || variant === "resolved";
|
|
80
80
|
const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
|
|
81
81
|
const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
|
|
82
|
-
const shouldRenderAvatar = Boolean(
|
|
82
|
+
const shouldRenderAvatar = Boolean(
|
|
83
|
+
avatar || avatarProps || resolvedAvatarName
|
|
84
|
+
);
|
|
83
85
|
const replyItems = (0, import_react.useMemo)(
|
|
84
|
-
() =>
|
|
86
|
+
() => flattenReplies(replies).filter(
|
|
85
87
|
(reply) => reply !== null && reply !== void 0 && reply !== false
|
|
86
88
|
),
|
|
87
89
|
[replies]
|
|
@@ -93,49 +95,55 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
|
|
|
93
95
|
setShowAllReplies(!shouldCollapseReplies);
|
|
94
96
|
}, [shouldCollapseReplies]);
|
|
95
97
|
const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
|
|
96
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
97
|
-
|
|
98
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
99
|
+
CommentContext.Provider,
|
|
98
100
|
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
101
|
+
value: { editable, onResolve, resolved: isResolved },
|
|
102
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
103
|
+
"div",
|
|
104
|
+
{
|
|
105
|
+
ref,
|
|
106
|
+
className: (0, import_utils.cx)(
|
|
107
|
+
"flex gap-12 rounded-default border p-12",
|
|
108
|
+
resolvedVariant,
|
|
109
|
+
className
|
|
110
|
+
),
|
|
111
|
+
"data-variant": variant,
|
|
112
|
+
...props,
|
|
113
|
+
children: [
|
|
114
|
+
shouldRenderAvatar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_avatars.Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
|
|
115
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
116
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-6", children: [
|
|
117
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
|
|
118
|
+
timestamp && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-fg-subtle", children: timestamp })
|
|
119
|
+
] }),
|
|
120
|
+
hasBodyContent && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
|
|
121
|
+
isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
|
|
122
|
+
actionsSlot ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-8", children: actionsSlot }) : null,
|
|
123
|
+
replies && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-[40px] border-l border-border-minimal pl-[20px]", children: [
|
|
124
|
+
shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
125
|
+
"button",
|
|
126
|
+
{
|
|
127
|
+
type: "button",
|
|
128
|
+
className: "mb-8 text-xs font-semibold text-fg-info-base",
|
|
129
|
+
onClick: () => setShowAllReplies(true),
|
|
130
|
+
"aria-expanded": showAllReplies,
|
|
131
|
+
children: [
|
|
132
|
+
"Show all ",
|
|
133
|
+
replyCount,
|
|
134
|
+
" replies"
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
visibleReplies
|
|
139
|
+
] }) : null
|
|
140
|
+
] })
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
)
|
|
136
144
|
}
|
|
137
|
-
)
|
|
138
|
-
}
|
|
145
|
+
);
|
|
146
|
+
}
|
|
139
147
|
var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
140
148
|
"div",
|
|
141
149
|
{
|
|
@@ -195,10 +203,13 @@ var CommentResolve = ({
|
|
|
195
203
|
}
|
|
196
204
|
);
|
|
197
205
|
};
|
|
198
|
-
Comment
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
var Comment = Object.assign((0, import_react.forwardRef)(CommentComponent), {
|
|
207
|
+
Actions: CommentActions,
|
|
208
|
+
Reply: CommentReply,
|
|
209
|
+
Edit: CommentEdit,
|
|
210
|
+
Resolve: CommentResolve
|
|
211
|
+
});
|
|
212
|
+
Comment.displayName = "Comment";
|
|
202
213
|
CommentActions.displayName = "Comment.Actions";
|
|
203
214
|
CommentReply.displayName = "Comment.Reply";
|
|
204
215
|
CommentEdit.displayName = "Comment.Edit";
|
|
@@ -220,9 +231,8 @@ var import_react3 = require("@tiptap/react");
|
|
|
220
231
|
var import_mentions = require("@edux-design/mentions");
|
|
221
232
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
222
233
|
var MentionNodeView = ({ node }) => {
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
|
|
234
|
+
const handle = node.attrs?.handle ?? "";
|
|
235
|
+
const label = node.attrs?.label ?? "";
|
|
226
236
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
227
237
|
import_react3.NodeViewWrapper,
|
|
228
238
|
{
|
|
@@ -265,6 +275,11 @@ var CommentMentionExtension = import_core.Node.create({
|
|
|
265
275
|
label
|
|
266
276
|
];
|
|
267
277
|
},
|
|
278
|
+
renderText({ node }) {
|
|
279
|
+
const handle = node.attrs.handle || "";
|
|
280
|
+
const label = node.attrs.label || "";
|
|
281
|
+
return `@${handle || label || "mention"}`;
|
|
282
|
+
},
|
|
268
283
|
addNodeView() {
|
|
269
284
|
return (0, import_react3.ReactNodeViewRenderer)(MentionNodeView);
|
|
270
285
|
}
|
|
@@ -282,87 +297,237 @@ var useCommentComposer = () => {
|
|
|
282
297
|
}
|
|
283
298
|
return context;
|
|
284
299
|
};
|
|
285
|
-
|
|
300
|
+
function readValue(editor) {
|
|
301
|
+
const mentions = [];
|
|
302
|
+
editor.state.doc.descendants((node) => {
|
|
303
|
+
if (node.type.name === "commentMention") {
|
|
304
|
+
mentions.push({
|
|
305
|
+
id: node.attrs.id ?? null,
|
|
306
|
+
handle: node.attrs.handle ?? "",
|
|
307
|
+
label: node.attrs.label ?? ""
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
const legacyText = editor.getText();
|
|
312
|
+
const text = editor.getText({
|
|
313
|
+
blockSeparator: "\n",
|
|
314
|
+
textSerializers: {
|
|
315
|
+
commentMention: ({ node }) => {
|
|
316
|
+
const { handle = "", label = "" } = node.attrs;
|
|
317
|
+
return label ? `@${handle} \xB7 ${label}` : `@${handle}`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}).replace(/\u00a0/g, " ").replace(/[\u200b-\u200d\ufeff]/g, "").replace(/\r?\n/g, " ");
|
|
321
|
+
return {
|
|
322
|
+
text,
|
|
323
|
+
legacyText,
|
|
324
|
+
html: editor.getHTML(),
|
|
325
|
+
json: editor.getJSON(),
|
|
326
|
+
mentions,
|
|
327
|
+
isEmpty: !text.trim()
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function stringContent(value, format) {
|
|
331
|
+
if (format !== "text") return value;
|
|
332
|
+
return {
|
|
333
|
+
type: "doc",
|
|
334
|
+
content: value.split(/\r?\n/).map((line) => ({
|
|
335
|
+
type: "paragraph",
|
|
336
|
+
...line ? { content: [{ type: "text", text: line }] } : {}
|
|
337
|
+
}))
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function CommentComposerComponent({
|
|
286
341
|
value,
|
|
342
|
+
valueFormat = "html",
|
|
343
|
+
richValue,
|
|
287
344
|
onChange,
|
|
345
|
+
onValueChange,
|
|
288
346
|
onSubmit,
|
|
289
347
|
onCancel,
|
|
348
|
+
composerRef,
|
|
349
|
+
editorLabel = "Comment",
|
|
350
|
+
editorAttributes: suppliedEditorAttributes,
|
|
351
|
+
autoFocus = false,
|
|
352
|
+
externalSubmit = false,
|
|
353
|
+
submitOnModEnter = false,
|
|
290
354
|
placeholder = "Write a comment...",
|
|
291
355
|
rows = 4,
|
|
292
356
|
disabled = false,
|
|
293
357
|
isSubmitting = false,
|
|
294
358
|
submitLabel = "Post",
|
|
359
|
+
submittingLabel = "Posting...",
|
|
295
360
|
cancelLabel = "Cancel",
|
|
296
361
|
avatar,
|
|
297
362
|
avatarProps,
|
|
298
363
|
actions,
|
|
299
364
|
className,
|
|
300
365
|
children,
|
|
366
|
+
onKeyDown,
|
|
367
|
+
onKeyDownCapture,
|
|
301
368
|
...props
|
|
302
369
|
}, ref) {
|
|
303
|
-
const
|
|
304
|
-
const
|
|
370
|
+
const containerRef = (0, import_react4.useRef)(null);
|
|
371
|
+
const formRef = (0, import_react4.useRef)(null);
|
|
372
|
+
const pendingFocusRef = (0, import_react4.useRef)(null);
|
|
373
|
+
const handledShortcutEvents = (0, import_react4.useRef)(/* @__PURE__ */ new WeakSet());
|
|
374
|
+
const callbacks = (0, import_react4.useRef)({ onChange, onValueChange });
|
|
375
|
+
callbacks.current = { onChange, onValueChange };
|
|
376
|
+
const pendingEchoesRef = (0, import_react4.useRef)([]);
|
|
377
|
+
const controlledRef = (0, import_react4.useRef)(false);
|
|
378
|
+
controlledRef.current = value !== void 0 || richValue !== void 0;
|
|
379
|
+
const placeholderRef = (0, import_react4.useRef)(placeholder);
|
|
380
|
+
placeholderRef.current = placeholder;
|
|
381
|
+
const [hasValue, setHasValue] = (0, import_react4.useState)(Boolean(richValue || value?.trim()));
|
|
382
|
+
const locked = disabled || isSubmitting;
|
|
383
|
+
const editorAttributes = (0, import_react4.useMemo)(
|
|
384
|
+
() => ({
|
|
385
|
+
...suppliedEditorAttributes,
|
|
386
|
+
"data-comment-editor": "true",
|
|
387
|
+
role: "textbox",
|
|
388
|
+
"aria-label": editorLabel,
|
|
389
|
+
"aria-multiline": "true",
|
|
390
|
+
"aria-disabled": String(locked),
|
|
391
|
+
class: (0, import_utils2.cx)(
|
|
392
|
+
"w-full bg-bg-base text-fg-base border-2 rounded-md border-border-base",
|
|
393
|
+
"outline-none transition-all duration-300 ease-in-out motion-reduce:transition-none",
|
|
394
|
+
"pl-[20px] py-12 font-normal text-base leading-relaxed overflow-y-auto",
|
|
395
|
+
"focus:shadow-focus focus-visible:shadow-focus",
|
|
396
|
+
locked && "bg-bg-subtle border-border-subtle text-fg-inactive"
|
|
397
|
+
),
|
|
398
|
+
style: `height: ${Math.max(1, rows) * 30}px`
|
|
399
|
+
}),
|
|
400
|
+
[editorLabel, locked, rows, suppliedEditorAttributes]
|
|
401
|
+
);
|
|
402
|
+
const isSubmitDisabled = locked || !hasValue;
|
|
305
403
|
const editor = (0, import_react5.useEditor)({
|
|
404
|
+
immediatelyRender: false,
|
|
306
405
|
extensions: [
|
|
307
406
|
import_starter_kit.default,
|
|
308
407
|
CommentMentionExtension,
|
|
309
|
-
import_extension_placeholder.default.configure({
|
|
310
|
-
placeholder
|
|
311
|
-
})
|
|
408
|
+
import_extension_placeholder.default.configure({ placeholder: () => placeholderRef.current })
|
|
312
409
|
],
|
|
313
|
-
content: value ?? "",
|
|
314
|
-
editable: !
|
|
315
|
-
editorProps: {
|
|
316
|
-
attributes: {
|
|
317
|
-
class: (0, import_utils2.cx)(
|
|
318
|
-
"w-full bg-white border-2 rounded-md border-border-base",
|
|
319
|
-
"outline-none transition-all duration-300 ease-in-out",
|
|
320
|
-
"pl-5 py-3 font-normal text-base leading-relaxed",
|
|
321
|
-
"h-[120px] overflow-y-auto",
|
|
322
|
-
"focus:shadow-focus focus-visible:shadow-focus",
|
|
323
|
-
disabled && "bg-bg-subtle border-border-subtle text-fg-inactive"
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
},
|
|
410
|
+
content: richValue ?? stringContent(value ?? "", valueFormat),
|
|
411
|
+
editable: !locked,
|
|
412
|
+
editorProps: { attributes: editorAttributes },
|
|
327
413
|
onUpdate: ({ editor: editor2 }) => {
|
|
328
|
-
|
|
414
|
+
const next = readValue(editor2);
|
|
415
|
+
if (controlledRef.current && (callbacks.current.onChange || callbacks.current.onValueChange)) {
|
|
416
|
+
pendingEchoesRef.current.push(next);
|
|
417
|
+
if (pendingEchoesRef.current.length > 100)
|
|
418
|
+
pendingEchoesRef.current.shift();
|
|
419
|
+
}
|
|
420
|
+
setHasValue(!next.isEmpty);
|
|
421
|
+
callbacks.current.onChange?.({ target: { value: next.legacyText } });
|
|
422
|
+
callbacks.current.onValueChange?.(next);
|
|
329
423
|
}
|
|
330
424
|
});
|
|
331
|
-
|
|
425
|
+
(0, import_react4.useImperativeHandle)(ref, () => containerRef.current, []);
|
|
426
|
+
(0, import_react4.useImperativeHandle)(
|
|
427
|
+
composerRef,
|
|
428
|
+
() => ({
|
|
429
|
+
focus: (position = "end") => {
|
|
430
|
+
if (locked) return false;
|
|
431
|
+
if (!editor) {
|
|
432
|
+
pendingFocusRef.current = position;
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
return editor.commands.focus(position);
|
|
436
|
+
},
|
|
437
|
+
clear: () => !locked && Boolean(editor?.commands.clearContent(true)),
|
|
438
|
+
getValue: () => editor ? readValue(editor) : null,
|
|
439
|
+
submit: () => {
|
|
440
|
+
if (!locked) formRef.current?.requestSubmit();
|
|
441
|
+
}
|
|
442
|
+
}),
|
|
443
|
+
[editor, locked]
|
|
444
|
+
);
|
|
332
445
|
(0, import_react4.useEffect)(() => {
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
446
|
+
if (!editor) return;
|
|
447
|
+
editor.setEditable(!locked, false);
|
|
448
|
+
editor.setOptions({
|
|
449
|
+
editorProps: {
|
|
450
|
+
...editor.options.editorProps,
|
|
451
|
+
attributes: editorAttributes
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}, [editor, locked, editorAttributes, placeholder]);
|
|
339
455
|
(0, import_react4.useEffect)(() => {
|
|
340
456
|
if (!editor) return;
|
|
341
|
-
|
|
342
|
-
|
|
457
|
+
const next = richValue ?? value;
|
|
458
|
+
if (next === void 0) return;
|
|
459
|
+
const current = readValue(editor);
|
|
460
|
+
const echoIndex = pendingEchoesRef.current.findIndex(
|
|
461
|
+
(snapshot) => richValue !== void 0 ? JSON.stringify(snapshot.json) === JSON.stringify(richValue) : snapshot.text === value || snapshot.legacyText === value
|
|
462
|
+
);
|
|
463
|
+
if (echoIndex !== -1) {
|
|
464
|
+
pendingEchoesRef.current.splice(0, echoIndex + 1);
|
|
465
|
+
setHasValue(!current.isEmpty);
|
|
466
|
+
return;
|
|
343
467
|
}
|
|
344
|
-
|
|
468
|
+
pendingEchoesRef.current = [];
|
|
469
|
+
const equal = richValue !== void 0 ? JSON.stringify(current.json) === JSON.stringify(richValue) : current.text === value || current.legacyText === value;
|
|
470
|
+
if (!equal)
|
|
471
|
+
editor.commands.setContent(
|
|
472
|
+
richValue ?? stringContent(value, valueFormat),
|
|
473
|
+
{ emitUpdate: false }
|
|
474
|
+
);
|
|
475
|
+
setHasValue(!readValue(editor).isEmpty);
|
|
476
|
+
}, [editor, value, richValue, valueFormat]);
|
|
477
|
+
(0, import_react4.useEffect)(() => {
|
|
478
|
+
if (editor) setHasValue(!readValue(editor).isEmpty);
|
|
479
|
+
}, [editor]);
|
|
480
|
+
(0, import_react4.useEffect)(() => {
|
|
481
|
+
if (!editor || locked) return;
|
|
482
|
+
if (!autoFocus && pendingFocusRef.current === null) return;
|
|
483
|
+
const position = pendingFocusRef.current ?? "end";
|
|
484
|
+
pendingFocusRef.current = null;
|
|
485
|
+
const frame = requestAnimationFrame(() => {
|
|
486
|
+
if (!editor.isDestroyed && editor.isEditable)
|
|
487
|
+
editor.commands.focus(position);
|
|
488
|
+
});
|
|
489
|
+
return () => cancelAnimationFrame(frame);
|
|
490
|
+
}, [editor, autoFocus, locked]);
|
|
345
491
|
const contextValue = (0, import_react4.useMemo)(
|
|
346
492
|
() => ({
|
|
347
493
|
editor,
|
|
348
494
|
placeholder,
|
|
495
|
+
editable: !locked,
|
|
349
496
|
editorContainerRef: containerRef
|
|
350
497
|
}),
|
|
351
|
-
[editor, placeholder]
|
|
498
|
+
[editor, placeholder, locked]
|
|
352
499
|
);
|
|
353
500
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
354
501
|
"form",
|
|
355
502
|
{
|
|
503
|
+
...props,
|
|
504
|
+
ref: formRef,
|
|
505
|
+
"data-comment-surface": "composer",
|
|
506
|
+
"aria-busy": isSubmitting || void 0,
|
|
356
507
|
className: (0, import_utils2.cx)(
|
|
357
508
|
"relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
|
|
358
509
|
className
|
|
359
510
|
),
|
|
511
|
+
onKeyDown: (event) => {
|
|
512
|
+
if (!handledShortcutEvents.current.has(event.nativeEvent))
|
|
513
|
+
onKeyDown?.(event);
|
|
514
|
+
},
|
|
515
|
+
onKeyDownCapture: (event) => {
|
|
516
|
+
onKeyDownCapture?.(event);
|
|
517
|
+
if (!event.defaultPrevented && submitOnModEnter && !event.nativeEvent.isComposing && (event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
518
|
+
handledShortcutEvents.current.add(event.nativeEvent);
|
|
519
|
+
onKeyDown?.(event);
|
|
520
|
+
if (event.defaultPrevented) return;
|
|
521
|
+
event.preventDefault();
|
|
522
|
+
if (!locked && editor && !readValue(editor).isEmpty)
|
|
523
|
+
formRef.current?.requestSubmit();
|
|
524
|
+
}
|
|
525
|
+
},
|
|
360
526
|
onSubmit: (event) => {
|
|
361
527
|
event.preventDefault();
|
|
362
|
-
if (
|
|
363
|
-
|
|
528
|
+
if (!locked && editor && !readValue(editor).isEmpty)
|
|
529
|
+
onSubmit?.(event);
|
|
364
530
|
},
|
|
365
|
-
...props,
|
|
366
531
|
children: [
|
|
367
532
|
(avatar || avatarProps) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_avatars2.Avatar, { size: "small", ...avatarProps }) }),
|
|
368
533
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -377,11 +542,11 @@ var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
|
|
|
377
542
|
size: "small",
|
|
378
543
|
isSecondary: true,
|
|
379
544
|
onClick: onCancel,
|
|
380
|
-
disabled:
|
|
545
|
+
disabled: locked,
|
|
381
546
|
children: cancelLabel
|
|
382
547
|
}
|
|
383
548
|
),
|
|
384
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_buttons.Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ?
|
|
549
|
+
!externalSubmit && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_buttons.Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? submittingLabel : submitLabel })
|
|
385
550
|
] })
|
|
386
551
|
] })
|
|
387
552
|
] }),
|
|
@@ -389,29 +554,32 @@ var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
|
|
|
389
554
|
]
|
|
390
555
|
}
|
|
391
556
|
) });
|
|
392
|
-
}
|
|
557
|
+
}
|
|
558
|
+
var CommentComposer = (0, import_react4.forwardRef)(CommentComposerComponent);
|
|
393
559
|
|
|
394
560
|
// src/elements/CommentMention.jsx
|
|
395
561
|
var import_react6 = __toESM(require("react"));
|
|
396
562
|
var import_menus = require("@edux-design/menus");
|
|
397
563
|
var import_avatars3 = require("@edux-design/avatars");
|
|
398
564
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
399
|
-
var CommentMention = ({
|
|
400
|
-
|
|
401
|
-
onQueryChange
|
|
402
|
-
}) => {
|
|
403
|
-
const { editor, editorContainerRef } = useCommentComposer();
|
|
565
|
+
var CommentMention = ({ peopleList = [], onQueryChange }) => {
|
|
566
|
+
const { editor, editorContainerRef, editable = true } = useCommentComposer();
|
|
404
567
|
const [menuOpen, setMenuOpen] = (0, import_react6.useState)(false);
|
|
405
568
|
const [query, setQuery] = (0, import_react6.useState)("");
|
|
406
|
-
const [range, setRange] = (0, import_react6.useState)(null);
|
|
407
569
|
const [anchorStyle, setAnchorStyle] = (0, import_react6.useState)({ left: 0, top: 0 });
|
|
408
570
|
const menuListRef = (0, import_react6.useRef)(null);
|
|
409
571
|
const rangeRef = (0, import_react6.useRef)(null);
|
|
410
572
|
const lastQueryRef = (0, import_react6.useRef)("");
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
573
|
+
const dismissedRangeRef = (0, import_react6.useRef)(null);
|
|
574
|
+
const focusFrameRef = (0, import_react6.useRef)(null);
|
|
575
|
+
const focusTimeoutRef = (0, import_react6.useRef)(null);
|
|
576
|
+
const dismissMenu = (0, import_react6.useCallback)(() => {
|
|
577
|
+
const activeRange = rangeRef.current;
|
|
578
|
+
dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
|
|
579
|
+
setMenuOpen(false);
|
|
580
|
+
if (editor && !editor.isDestroyed && editor.isEditable !== false)
|
|
581
|
+
editor.view.focus();
|
|
582
|
+
}, [editor]);
|
|
415
583
|
const filteredPeople = (0, import_react6.useMemo)(() => {
|
|
416
584
|
if (!query) return peopleList;
|
|
417
585
|
const lowered = query.toLowerCase();
|
|
@@ -420,14 +588,15 @@ var CommentMention = ({
|
|
|
420
588
|
);
|
|
421
589
|
}, [peopleList, query]);
|
|
422
590
|
const updateMenuState = (0, import_react6.useCallback)(() => {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
591
|
+
if (!editor || !editable || editor.isEditable === false) {
|
|
592
|
+
setMenuOpen(false);
|
|
593
|
+
rangeRef.current = null;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
426
596
|
const { parent, parentOffset, pos } = editor.state.selection.$from;
|
|
427
597
|
if (!parent.isTextblock) {
|
|
428
598
|
setMenuOpen(false);
|
|
429
599
|
setQuery("");
|
|
430
|
-
setRange(null);
|
|
431
600
|
return;
|
|
432
601
|
}
|
|
433
602
|
const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
|
|
@@ -439,13 +608,12 @@ var CommentMention = ({
|
|
|
439
608
|
onQueryChange(handleQuery);
|
|
440
609
|
}
|
|
441
610
|
setQuery(handleQuery);
|
|
442
|
-
setMenuOpen(true);
|
|
443
611
|
const fromPos = pos - match[0].length;
|
|
444
612
|
const nextRange = { from: fromPos, to: pos };
|
|
445
|
-
|
|
613
|
+
setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
|
|
446
614
|
rangeRef.current = nextRange;
|
|
447
615
|
const coords = editor.view.coordsAtPos(fromPos);
|
|
448
|
-
const containerRect =
|
|
616
|
+
const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
|
|
449
617
|
if (containerRect) {
|
|
450
618
|
setAnchorStyle({
|
|
451
619
|
left: coords.left - containerRect.left + 12,
|
|
@@ -456,16 +624,16 @@ var CommentMention = ({
|
|
|
456
624
|
}
|
|
457
625
|
setMenuOpen(false);
|
|
458
626
|
setQuery("");
|
|
459
|
-
setRange(null);
|
|
460
627
|
rangeRef.current = null;
|
|
461
628
|
if (onQueryChange && lastQueryRef.current !== "") {
|
|
462
629
|
lastQueryRef.current = "";
|
|
463
630
|
onQueryChange("");
|
|
464
631
|
}
|
|
465
|
-
}, [editor, editorContainerRef, onQueryChange]);
|
|
632
|
+
}, [editor, editorContainerRef, onQueryChange, editable]);
|
|
466
633
|
const handleSelectPerson = (0, import_react6.useCallback)(
|
|
467
634
|
(person) => {
|
|
468
|
-
if (!editor || !rangeRef.current)
|
|
635
|
+
if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
|
|
636
|
+
return;
|
|
469
637
|
const selectionRange = rangeRef.current;
|
|
470
638
|
editor.commands.focus();
|
|
471
639
|
editor.commands.setTextSelection(selectionRange);
|
|
@@ -485,21 +653,34 @@ var CommentMention = ({
|
|
|
485
653
|
{ updateSelection: false }
|
|
486
654
|
);
|
|
487
655
|
const nextPos = selectionRange.from + 2;
|
|
488
|
-
|
|
656
|
+
editor.view.focus();
|
|
657
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
658
|
+
clearTimeout(focusTimeoutRef.current);
|
|
659
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
660
|
+
if (editor.isDestroyed || editor.isEditable === false) return;
|
|
489
661
|
editor.commands.setTextSelection(nextPos);
|
|
490
662
|
editor.view.focus();
|
|
491
|
-
setTimeout(() =>
|
|
663
|
+
focusTimeoutRef.current = setTimeout(() => {
|
|
664
|
+
if (!editor.isDestroyed && editor.isEditable !== false)
|
|
665
|
+
editor.view.focus();
|
|
666
|
+
}, 0);
|
|
492
667
|
});
|
|
493
668
|
setMenuOpen(false);
|
|
494
669
|
setQuery("");
|
|
495
|
-
setRange(null);
|
|
496
670
|
rangeRef.current = null;
|
|
497
671
|
if (onQueryChange && lastQueryRef.current !== "") {
|
|
498
672
|
lastQueryRef.current = "";
|
|
499
673
|
onQueryChange("");
|
|
500
674
|
}
|
|
501
675
|
},
|
|
502
|
-
[editor, onQueryChange]
|
|
676
|
+
[editor, onQueryChange, editable]
|
|
677
|
+
);
|
|
678
|
+
(0, import_react6.useEffect)(
|
|
679
|
+
() => () => {
|
|
680
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
681
|
+
clearTimeout(focusTimeoutRef.current);
|
|
682
|
+
},
|
|
683
|
+
[editor]
|
|
503
684
|
);
|
|
504
685
|
(0, import_react6.useEffect)(() => {
|
|
505
686
|
if (!editor) return;
|
|
@@ -512,25 +693,30 @@ var CommentMention = ({
|
|
|
512
693
|
};
|
|
513
694
|
}, [editor, updateMenuState]);
|
|
514
695
|
(0, import_react6.useEffect)(() => {
|
|
515
|
-
var _a;
|
|
516
696
|
if (!editor) return;
|
|
517
697
|
const handleKeyDown = (event) => {
|
|
518
|
-
var _a2;
|
|
519
698
|
if (!menuOpen) return;
|
|
699
|
+
if (event.key === "Escape") {
|
|
700
|
+
event.preventDefault();
|
|
701
|
+
event.stopPropagation();
|
|
702
|
+
dismissMenu();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
520
705
|
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
521
706
|
event.preventDefault();
|
|
707
|
+
event.stopPropagation();
|
|
522
708
|
const list = menuListRef.current;
|
|
523
|
-
const items = list
|
|
524
|
-
if (!list || !
|
|
709
|
+
const items = list?.querySelectorAll("[data-menu-item]");
|
|
710
|
+
if (!list || !items?.length) return;
|
|
525
711
|
list.focus();
|
|
526
712
|
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
527
713
|
const targetIndex = direction === 1 ? 0 : items.length - 1;
|
|
528
|
-
|
|
714
|
+
items[targetIndex]?.focus();
|
|
529
715
|
};
|
|
530
|
-
const dom =
|
|
531
|
-
dom
|
|
532
|
-
return () => dom
|
|
533
|
-
}, [editor, menuOpen]);
|
|
716
|
+
const dom = editor.view?.dom;
|
|
717
|
+
dom?.addEventListener("keydown", handleKeyDown, true);
|
|
718
|
+
return () => dom?.removeEventListener("keydown", handleKeyDown, true);
|
|
719
|
+
}, [editor, menuOpen, dismissMenu]);
|
|
534
720
|
const menuContent = (0, import_react6.useMemo)(
|
|
535
721
|
() => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
536
722
|
import_menus.Menu,
|
|
@@ -542,6 +728,7 @@ var CommentMention = ({
|
|
|
542
728
|
{
|
|
543
729
|
type: "button",
|
|
544
730
|
"aria-hidden": true,
|
|
731
|
+
tabIndex: -1,
|
|
545
732
|
className: "absolute h-1 w-1 opacity-0",
|
|
546
733
|
style: {
|
|
547
734
|
left: `${anchorStyle.left}px`,
|
|
@@ -561,32 +748,46 @@ var CommentMention = ({
|
|
|
561
748
|
collisionPadding: 12,
|
|
562
749
|
onCloseAutoFocus: (event) => event.preventDefault()
|
|
563
750
|
},
|
|
564
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
565
|
-
import_menus.
|
|
751
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
752
|
+
import_menus.MenuList,
|
|
566
753
|
{
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
754
|
+
ref: menuListRef,
|
|
755
|
+
className: "min-w-[220px]",
|
|
756
|
+
onKeyDownCapture: (event) => {
|
|
757
|
+
if (event.key === "Escape") {
|
|
758
|
+
event.preventDefault();
|
|
759
|
+
event.stopPropagation();
|
|
760
|
+
dismissMenu();
|
|
761
|
+
}
|
|
571
762
|
},
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
763
|
+
children: filteredPeople.map((person) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
764
|
+
import_menus.MenuItem,
|
|
765
|
+
{
|
|
766
|
+
onMouseDown: (event) => {
|
|
767
|
+
event.preventDefault();
|
|
768
|
+
event.stopPropagation();
|
|
769
|
+
handleSelectPerson(person);
|
|
770
|
+
},
|
|
771
|
+
onClick: () => handleSelectPerson(person),
|
|
772
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
773
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-8", children: [
|
|
774
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_avatars3.Avatar, { size: "small", ...person.avatarProps }),
|
|
775
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex flex-col", children: [
|
|
776
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm text-fg-base", children: person.name }),
|
|
777
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-xs text-fg-subtle", children: [
|
|
778
|
+
"@",
|
|
779
|
+
person.handle
|
|
780
|
+
] })
|
|
781
|
+
] })
|
|
581
782
|
] })
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
783
|
+
},
|
|
784
|
+
person.id
|
|
785
|
+
))
|
|
786
|
+
}
|
|
787
|
+
)
|
|
587
788
|
}
|
|
588
789
|
),
|
|
589
|
-
[filteredPeople, handleSelectPerson, menuOpen]
|
|
790
|
+
[filteredPeople, handleSelectPerson, menuOpen, anchorStyle, dismissMenu]
|
|
590
791
|
);
|
|
591
792
|
if (!editor) return null;
|
|
592
793
|
return menuContent;
|