@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.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,45 +554,108 @@ 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
|
-
var
|
|
561
|
+
var import_react7 = __toESM(require("react"));
|
|
396
562
|
var import_menus = require("@edux-design/menus");
|
|
397
563
|
var import_avatars3 = require("@edux-design/avatars");
|
|
564
|
+
|
|
565
|
+
// src/elements/useMentionPortalContainer.js
|
|
566
|
+
var import_react6 = require("react");
|
|
567
|
+
function resolveMentionPortalContainer(anchor, override) {
|
|
568
|
+
const document = anchor?.ownerDocument;
|
|
569
|
+
if (!document || !anchor.isConnected) return void 0;
|
|
570
|
+
if (override === null) return void 0;
|
|
571
|
+
if (override?.isConnected && override.ownerDocument === document)
|
|
572
|
+
return override;
|
|
573
|
+
const dialog = anchor.closest(
|
|
574
|
+
'[role="dialog"], [role="alertdialog"], dialog'
|
|
575
|
+
);
|
|
576
|
+
return dialog?.isConnected ? dialog : void 0;
|
|
577
|
+
}
|
|
578
|
+
function useMentionPortalContainer(anchorRef, override, active) {
|
|
579
|
+
const [container, setContainer] = (0, import_react6.useState)(void 0);
|
|
580
|
+
(0, import_react6.useLayoutEffect)(() => {
|
|
581
|
+
let disposed = false;
|
|
582
|
+
let observer;
|
|
583
|
+
let observedDocument;
|
|
584
|
+
const refresh = () => {
|
|
585
|
+
if (disposed) return;
|
|
586
|
+
const anchor = anchorRef?.current;
|
|
587
|
+
const next = resolveMentionPortalContainer(anchor, override);
|
|
588
|
+
setContainer((previous) => previous === next ? previous : next);
|
|
589
|
+
const document = anchor?.ownerDocument;
|
|
590
|
+
if (!active || observedDocument === document) return;
|
|
591
|
+
observer?.disconnect();
|
|
592
|
+
observedDocument = document;
|
|
593
|
+
const MutationObserver = document?.defaultView?.MutationObserver;
|
|
594
|
+
if (MutationObserver && document.documentElement) {
|
|
595
|
+
observer = new MutationObserver(refresh);
|
|
596
|
+
observer.observe(document.documentElement, {
|
|
597
|
+
childList: true,
|
|
598
|
+
subtree: true,
|
|
599
|
+
attributes: true,
|
|
600
|
+
attributeFilter: ["role"]
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
refresh();
|
|
605
|
+
return () => {
|
|
606
|
+
disposed = true;
|
|
607
|
+
observer?.disconnect();
|
|
608
|
+
};
|
|
609
|
+
}, [anchorRef, override, active]);
|
|
610
|
+
return container;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/elements/CommentMention.jsx
|
|
398
614
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
399
615
|
var CommentMention = ({
|
|
400
616
|
peopleList = [],
|
|
401
|
-
onQueryChange
|
|
617
|
+
onQueryChange,
|
|
618
|
+
portalContainer
|
|
402
619
|
}) => {
|
|
403
|
-
const { editor, editorContainerRef } = useCommentComposer();
|
|
404
|
-
const [menuOpen, setMenuOpen] = (0,
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const rangeRef = (0, import_react6.useRef)(null);
|
|
410
|
-
const lastQueryRef = (0, import_react6.useRef)("");
|
|
411
|
-
const peopleByHandle = (0, import_react6.useMemo)(
|
|
412
|
-
() => new Map(peopleList.map((person) => [person.handle.toLowerCase(), person])),
|
|
413
|
-
[peopleList]
|
|
620
|
+
const { editor, editorContainerRef, editable = true } = useCommentComposer();
|
|
621
|
+
const [menuOpen, setMenuOpen] = (0, import_react7.useState)(false);
|
|
622
|
+
const mentionPortalContainer = useMentionPortalContainer(
|
|
623
|
+
editorContainerRef,
|
|
624
|
+
portalContainer,
|
|
625
|
+
menuOpen
|
|
414
626
|
);
|
|
415
|
-
const
|
|
627
|
+
const [query, setQuery] = (0, import_react7.useState)("");
|
|
628
|
+
const [anchorStyle, setAnchorStyle] = (0, import_react7.useState)({ left: 0, top: 0 });
|
|
629
|
+
const menuListRef = (0, import_react7.useRef)(null);
|
|
630
|
+
const rangeRef = (0, import_react7.useRef)(null);
|
|
631
|
+
const lastQueryRef = (0, import_react7.useRef)("");
|
|
632
|
+
const dismissedRangeRef = (0, import_react7.useRef)(null);
|
|
633
|
+
const focusFrameRef = (0, import_react7.useRef)(null);
|
|
634
|
+
const focusTimeoutRef = (0, import_react7.useRef)(null);
|
|
635
|
+
const dismissMenu = (0, import_react7.useCallback)(() => {
|
|
636
|
+
const activeRange = rangeRef.current;
|
|
637
|
+
dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
|
|
638
|
+
setMenuOpen(false);
|
|
639
|
+
if (editor && !editor.isDestroyed && editor.isEditable !== false)
|
|
640
|
+
editor.view.focus();
|
|
641
|
+
}, [editor]);
|
|
642
|
+
const filteredPeople = (0, import_react7.useMemo)(() => {
|
|
416
643
|
if (!query) return peopleList;
|
|
417
644
|
const lowered = query.toLowerCase();
|
|
418
645
|
return peopleList.filter(
|
|
419
646
|
(person) => person.handle.toLowerCase().startsWith(lowered)
|
|
420
647
|
);
|
|
421
648
|
}, [peopleList, query]);
|
|
422
|
-
const updateMenuState = (0,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
649
|
+
const updateMenuState = (0, import_react7.useCallback)(() => {
|
|
650
|
+
if (!editor || !editable || editor.isEditable === false) {
|
|
651
|
+
setMenuOpen(false);
|
|
652
|
+
rangeRef.current = null;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
426
655
|
const { parent, parentOffset, pos } = editor.state.selection.$from;
|
|
427
656
|
if (!parent.isTextblock) {
|
|
428
657
|
setMenuOpen(false);
|
|
429
658
|
setQuery("");
|
|
430
|
-
setRange(null);
|
|
431
659
|
return;
|
|
432
660
|
}
|
|
433
661
|
const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
|
|
@@ -439,13 +667,12 @@ var CommentMention = ({
|
|
|
439
667
|
onQueryChange(handleQuery);
|
|
440
668
|
}
|
|
441
669
|
setQuery(handleQuery);
|
|
442
|
-
setMenuOpen(true);
|
|
443
670
|
const fromPos = pos - match[0].length;
|
|
444
671
|
const nextRange = { from: fromPos, to: pos };
|
|
445
|
-
|
|
672
|
+
setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
|
|
446
673
|
rangeRef.current = nextRange;
|
|
447
674
|
const coords = editor.view.coordsAtPos(fromPos);
|
|
448
|
-
const containerRect =
|
|
675
|
+
const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
|
|
449
676
|
if (containerRect) {
|
|
450
677
|
setAnchorStyle({
|
|
451
678
|
left: coords.left - containerRect.left + 12,
|
|
@@ -456,16 +683,16 @@ var CommentMention = ({
|
|
|
456
683
|
}
|
|
457
684
|
setMenuOpen(false);
|
|
458
685
|
setQuery("");
|
|
459
|
-
setRange(null);
|
|
460
686
|
rangeRef.current = null;
|
|
461
687
|
if (onQueryChange && lastQueryRef.current !== "") {
|
|
462
688
|
lastQueryRef.current = "";
|
|
463
689
|
onQueryChange("");
|
|
464
690
|
}
|
|
465
|
-
}, [editor, editorContainerRef, onQueryChange]);
|
|
466
|
-
const handleSelectPerson = (0,
|
|
691
|
+
}, [editor, editorContainerRef, onQueryChange, editable]);
|
|
692
|
+
const handleSelectPerson = (0, import_react7.useCallback)(
|
|
467
693
|
(person) => {
|
|
468
|
-
if (!editor || !rangeRef.current)
|
|
694
|
+
if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
|
|
695
|
+
return;
|
|
469
696
|
const selectionRange = rangeRef.current;
|
|
470
697
|
editor.commands.focus();
|
|
471
698
|
editor.commands.setTextSelection(selectionRange);
|
|
@@ -485,23 +712,36 @@ var CommentMention = ({
|
|
|
485
712
|
{ updateSelection: false }
|
|
486
713
|
);
|
|
487
714
|
const nextPos = selectionRange.from + 2;
|
|
488
|
-
|
|
715
|
+
editor.view.focus();
|
|
716
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
717
|
+
clearTimeout(focusTimeoutRef.current);
|
|
718
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
719
|
+
if (editor.isDestroyed || editor.isEditable === false) return;
|
|
489
720
|
editor.commands.setTextSelection(nextPos);
|
|
490
721
|
editor.view.focus();
|
|
491
|
-
setTimeout(() =>
|
|
722
|
+
focusTimeoutRef.current = setTimeout(() => {
|
|
723
|
+
if (!editor.isDestroyed && editor.isEditable !== false)
|
|
724
|
+
editor.view.focus();
|
|
725
|
+
}, 0);
|
|
492
726
|
});
|
|
493
727
|
setMenuOpen(false);
|
|
494
728
|
setQuery("");
|
|
495
|
-
setRange(null);
|
|
496
729
|
rangeRef.current = null;
|
|
497
730
|
if (onQueryChange && lastQueryRef.current !== "") {
|
|
498
731
|
lastQueryRef.current = "";
|
|
499
732
|
onQueryChange("");
|
|
500
733
|
}
|
|
501
734
|
},
|
|
502
|
-
[editor, onQueryChange]
|
|
735
|
+
[editor, onQueryChange, editable]
|
|
503
736
|
);
|
|
504
|
-
(0,
|
|
737
|
+
(0, import_react7.useEffect)(
|
|
738
|
+
() => () => {
|
|
739
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
740
|
+
clearTimeout(focusTimeoutRef.current);
|
|
741
|
+
},
|
|
742
|
+
[editor]
|
|
743
|
+
);
|
|
744
|
+
(0, import_react7.useEffect)(() => {
|
|
505
745
|
if (!editor) return;
|
|
506
746
|
updateMenuState();
|
|
507
747
|
editor.on("transaction", updateMenuState);
|
|
@@ -511,27 +751,32 @@ var CommentMention = ({
|
|
|
511
751
|
editor.off("selectionUpdate", updateMenuState);
|
|
512
752
|
};
|
|
513
753
|
}, [editor, updateMenuState]);
|
|
514
|
-
(0,
|
|
515
|
-
var _a;
|
|
754
|
+
(0, import_react7.useEffect)(() => {
|
|
516
755
|
if (!editor) return;
|
|
517
756
|
const handleKeyDown = (event) => {
|
|
518
|
-
var _a2;
|
|
519
757
|
if (!menuOpen) return;
|
|
758
|
+
if (event.key === "Escape") {
|
|
759
|
+
event.preventDefault();
|
|
760
|
+
event.stopPropagation();
|
|
761
|
+
dismissMenu();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
520
764
|
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
521
765
|
event.preventDefault();
|
|
766
|
+
event.stopPropagation();
|
|
522
767
|
const list = menuListRef.current;
|
|
523
|
-
const items = list
|
|
524
|
-
if (!list || !
|
|
768
|
+
const items = list?.querySelectorAll("[data-menu-item]");
|
|
769
|
+
if (!list || !items?.length) return;
|
|
525
770
|
list.focus();
|
|
526
771
|
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
527
772
|
const targetIndex = direction === 1 ? 0 : items.length - 1;
|
|
528
|
-
|
|
773
|
+
items[targetIndex]?.focus();
|
|
529
774
|
};
|
|
530
|
-
const dom =
|
|
531
|
-
dom
|
|
532
|
-
return () => dom
|
|
533
|
-
}, [editor, menuOpen]);
|
|
534
|
-
const menuContent = (0,
|
|
775
|
+
const dom = editor.view?.dom;
|
|
776
|
+
dom?.addEventListener("keydown", handleKeyDown, true);
|
|
777
|
+
return () => dom?.removeEventListener("keydown", handleKeyDown, true);
|
|
778
|
+
}, [editor, menuOpen, dismissMenu]);
|
|
779
|
+
const menuContent = (0, import_react7.useMemo)(
|
|
535
780
|
() => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
536
781
|
import_menus.Menu,
|
|
537
782
|
{
|
|
@@ -542,6 +787,7 @@ var CommentMention = ({
|
|
|
542
787
|
{
|
|
543
788
|
type: "button",
|
|
544
789
|
"aria-hidden": true,
|
|
790
|
+
tabIndex: -1,
|
|
545
791
|
className: "absolute h-1 w-1 opacity-0",
|
|
546
792
|
style: {
|
|
547
793
|
left: `${anchorStyle.left}px`,
|
|
@@ -559,34 +805,56 @@ var CommentMention = ({
|
|
|
559
805
|
sideOffset: 4,
|
|
560
806
|
avoidCollisions: true,
|
|
561
807
|
collisionPadding: 12,
|
|
808
|
+
container: mentionPortalContainer,
|
|
562
809
|
onCloseAutoFocus: (event) => event.preventDefault()
|
|
563
810
|
},
|
|
564
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
565
|
-
import_menus.
|
|
811
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
812
|
+
import_menus.MenuList,
|
|
566
813
|
{
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
814
|
+
ref: menuListRef,
|
|
815
|
+
className: "min-w-[220px]",
|
|
816
|
+
onKeyDownCapture: (event) => {
|
|
817
|
+
if (event.key === "Escape") {
|
|
818
|
+
event.preventDefault();
|
|
819
|
+
event.stopPropagation();
|
|
820
|
+
dismissMenu();
|
|
821
|
+
}
|
|
571
822
|
},
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
823
|
+
children: filteredPeople.map((person) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
824
|
+
import_menus.MenuItem,
|
|
825
|
+
{
|
|
826
|
+
onMouseDown: (event) => {
|
|
827
|
+
event.preventDefault();
|
|
828
|
+
event.stopPropagation();
|
|
829
|
+
handleSelectPerson(person);
|
|
830
|
+
},
|
|
831
|
+
onClick: () => handleSelectPerson(person),
|
|
832
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
833
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-8", children: [
|
|
834
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_avatars3.Avatar, { size: "small", ...person.avatarProps }),
|
|
835
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex flex-col", children: [
|
|
836
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm text-fg-base", children: person.name }),
|
|
837
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-xs text-fg-subtle", children: [
|
|
838
|
+
"@",
|
|
839
|
+
person.handle
|
|
840
|
+
] })
|
|
841
|
+
] })
|
|
581
842
|
] })
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
843
|
+
},
|
|
844
|
+
person.id
|
|
845
|
+
))
|
|
846
|
+
}
|
|
847
|
+
)
|
|
587
848
|
}
|
|
588
849
|
),
|
|
589
|
-
[
|
|
850
|
+
[
|
|
851
|
+
filteredPeople,
|
|
852
|
+
handleSelectPerson,
|
|
853
|
+
menuOpen,
|
|
854
|
+
anchorStyle,
|
|
855
|
+
dismissMenu,
|
|
856
|
+
mentionPortalContainer
|
|
857
|
+
]
|
|
590
858
|
);
|
|
591
859
|
if (!editor) return null;
|
|
592
860
|
return menuContent;
|