@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.js
CHANGED
|
@@ -46,10 +46,14 @@ var VARIANT_STYLES = {
|
|
|
46
46
|
};
|
|
47
47
|
var CommentContext = (0, import_react.createContext)({
|
|
48
48
|
editable: true,
|
|
49
|
-
onResolve: void 0
|
|
49
|
+
onResolve: void 0,
|
|
50
|
+
resolved: false
|
|
50
51
|
});
|
|
51
52
|
var useCommentContext = () => (0, import_react.useContext)(CommentContext);
|
|
52
|
-
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({
|
|
53
57
|
author = "Author",
|
|
54
58
|
timestamp,
|
|
55
59
|
avatar,
|
|
@@ -67,10 +71,7 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
|
|
|
67
71
|
}, ref) {
|
|
68
72
|
const childArray = import_react.default.Children.toArray(children);
|
|
69
73
|
const actionsSlot = childArray.find(
|
|
70
|
-
(child) =>
|
|
71
|
-
var _a;
|
|
72
|
-
return (0, import_react.isValidElement)(child) && (child.type === CommentActions || ((_a = child.type) == null ? void 0 : _a.displayName) === "Comment.Actions");
|
|
73
|
-
}
|
|
74
|
+
(child) => (0, import_react.isValidElement)(child) && (child.type === CommentActions || child.type?.displayName === "Comment.Actions")
|
|
74
75
|
);
|
|
75
76
|
const bodyNodes = childArray.filter((child) => child !== actionsSlot);
|
|
76
77
|
const bodyContent = body ?? bodyNodes;
|
|
@@ -78,9 +79,11 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
|
|
|
78
79
|
const isResolved = resolved || variant === "resolved";
|
|
79
80
|
const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
|
|
80
81
|
const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
|
|
81
|
-
const shouldRenderAvatar = Boolean(
|
|
82
|
+
const shouldRenderAvatar = Boolean(
|
|
83
|
+
avatar || avatarProps || resolvedAvatarName
|
|
84
|
+
);
|
|
82
85
|
const replyItems = (0, import_react.useMemo)(
|
|
83
|
-
() =>
|
|
86
|
+
() => flattenReplies(replies).filter(
|
|
84
87
|
(reply) => reply !== null && reply !== void 0 && reply !== false
|
|
85
88
|
),
|
|
86
89
|
[replies]
|
|
@@ -92,49 +95,55 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
|
|
|
92
95
|
setShowAllReplies(!shouldCollapseReplies);
|
|
93
96
|
}, [shouldCollapseReplies]);
|
|
94
97
|
const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
|
|
95
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
96
|
-
|
|
98
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
99
|
+
CommentContext.Provider,
|
|
97
100
|
{
|
|
98
|
-
|
|
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
|
-
|
|
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
|
+
)
|
|
135
144
|
}
|
|
136
|
-
)
|
|
137
|
-
}
|
|
145
|
+
);
|
|
146
|
+
}
|
|
138
147
|
var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
139
148
|
"div",
|
|
140
149
|
{
|
|
@@ -168,31 +177,39 @@ var CommentResolve = ({
|
|
|
168
177
|
className,
|
|
169
178
|
disabled,
|
|
170
179
|
onClick,
|
|
180
|
+
label,
|
|
171
181
|
...props
|
|
172
182
|
}) => {
|
|
173
|
-
const { editable, onResolve } = useCommentContext();
|
|
183
|
+
const { editable, onResolve, resolved } = useCommentContext();
|
|
174
184
|
const isDisabled = disabled || !editable;
|
|
175
185
|
const handleClick = onClick ?? onResolve;
|
|
186
|
+
const isResolved = Boolean(resolved);
|
|
187
|
+
const actionLabel = label ?? (isResolved ? "Re-open" : "Resolve");
|
|
188
|
+
const toneClass = isResolved ? "text-fg-info-base" : "text-fg-success-base";
|
|
176
189
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
177
190
|
"button",
|
|
178
191
|
{
|
|
179
192
|
type: "button",
|
|
180
193
|
className: (0, import_utils.cx)(
|
|
181
|
-
"text-xs font-semibold
|
|
194
|
+
"text-xs font-semibold cursor-pointer",
|
|
195
|
+
toneClass,
|
|
182
196
|
className
|
|
183
197
|
),
|
|
184
198
|
onClick: handleClick,
|
|
185
199
|
disabled: isDisabled,
|
|
186
200
|
"aria-disabled": isDisabled,
|
|
187
201
|
...props,
|
|
188
|
-
children:
|
|
202
|
+
children: actionLabel
|
|
189
203
|
}
|
|
190
204
|
);
|
|
191
205
|
};
|
|
192
|
-
Comment
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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";
|
|
196
213
|
CommentActions.displayName = "Comment.Actions";
|
|
197
214
|
CommentReply.displayName = "Comment.Reply";
|
|
198
215
|
CommentEdit.displayName = "Comment.Edit";
|
|
@@ -214,9 +231,8 @@ var import_react3 = require("@tiptap/react");
|
|
|
214
231
|
var import_mentions = require("@edux-design/mentions");
|
|
215
232
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
216
233
|
var MentionNodeView = ({ node }) => {
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
|
|
234
|
+
const handle = node.attrs?.handle ?? "";
|
|
235
|
+
const label = node.attrs?.label ?? "";
|
|
220
236
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
221
237
|
import_react3.NodeViewWrapper,
|
|
222
238
|
{
|
|
@@ -259,6 +275,11 @@ var CommentMentionExtension = import_core.Node.create({
|
|
|
259
275
|
label
|
|
260
276
|
];
|
|
261
277
|
},
|
|
278
|
+
renderText({ node }) {
|
|
279
|
+
const handle = node.attrs.handle || "";
|
|
280
|
+
const label = node.attrs.label || "";
|
|
281
|
+
return `@${handle || label || "mention"}`;
|
|
282
|
+
},
|
|
262
283
|
addNodeView() {
|
|
263
284
|
return (0, import_react3.ReactNodeViewRenderer)(MentionNodeView);
|
|
264
285
|
}
|
|
@@ -276,87 +297,237 @@ var useCommentComposer = () => {
|
|
|
276
297
|
}
|
|
277
298
|
return context;
|
|
278
299
|
};
|
|
279
|
-
|
|
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({
|
|
280
341
|
value,
|
|
342
|
+
valueFormat = "html",
|
|
343
|
+
richValue,
|
|
281
344
|
onChange,
|
|
345
|
+
onValueChange,
|
|
282
346
|
onSubmit,
|
|
283
347
|
onCancel,
|
|
348
|
+
composerRef,
|
|
349
|
+
editorLabel = "Comment",
|
|
350
|
+
editorAttributes: suppliedEditorAttributes,
|
|
351
|
+
autoFocus = false,
|
|
352
|
+
externalSubmit = false,
|
|
353
|
+
submitOnModEnter = false,
|
|
284
354
|
placeholder = "Write a comment...",
|
|
285
355
|
rows = 4,
|
|
286
356
|
disabled = false,
|
|
287
357
|
isSubmitting = false,
|
|
288
358
|
submitLabel = "Post",
|
|
359
|
+
submittingLabel = "Posting...",
|
|
289
360
|
cancelLabel = "Cancel",
|
|
290
361
|
avatar,
|
|
291
362
|
avatarProps,
|
|
292
363
|
actions,
|
|
293
364
|
className,
|
|
294
365
|
children,
|
|
366
|
+
onKeyDown,
|
|
367
|
+
onKeyDownCapture,
|
|
295
368
|
...props
|
|
296
369
|
}, ref) {
|
|
297
|
-
const
|
|
298
|
-
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;
|
|
299
403
|
const editor = (0, import_react5.useEditor)({
|
|
404
|
+
immediatelyRender: false,
|
|
300
405
|
extensions: [
|
|
301
406
|
import_starter_kit.default,
|
|
302
407
|
CommentMentionExtension,
|
|
303
|
-
import_extension_placeholder.default.configure({
|
|
304
|
-
placeholder
|
|
305
|
-
})
|
|
408
|
+
import_extension_placeholder.default.configure({ placeholder: () => placeholderRef.current })
|
|
306
409
|
],
|
|
307
|
-
content: value ?? "",
|
|
308
|
-
editable: !
|
|
309
|
-
editorProps: {
|
|
310
|
-
attributes: {
|
|
311
|
-
class: (0, import_utils2.cx)(
|
|
312
|
-
"w-full bg-white border-2 rounded-md border-border-base",
|
|
313
|
-
"outline-none transition-all duration-300 ease-in-out",
|
|
314
|
-
"pl-5 py-3 font-normal text-base leading-relaxed",
|
|
315
|
-
"h-[120px] overflow-y-auto",
|
|
316
|
-
"focus:shadow-focus focus-visible:shadow-focus",
|
|
317
|
-
disabled && "bg-bg-subtle border-border-subtle text-fg-inactive"
|
|
318
|
-
)
|
|
319
|
-
}
|
|
320
|
-
},
|
|
410
|
+
content: richValue ?? stringContent(value ?? "", valueFormat),
|
|
411
|
+
editable: !locked,
|
|
412
|
+
editorProps: { attributes: editorAttributes },
|
|
321
413
|
onUpdate: ({ editor: editor2 }) => {
|
|
322
|
-
|
|
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);
|
|
323
423
|
}
|
|
324
424
|
});
|
|
325
|
-
|
|
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
|
+
);
|
|
326
445
|
(0, import_react4.useEffect)(() => {
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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]);
|
|
333
455
|
(0, import_react4.useEffect)(() => {
|
|
334
456
|
if (!editor) return;
|
|
335
|
-
|
|
336
|
-
|
|
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;
|
|
337
467
|
}
|
|
338
|
-
|
|
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]);
|
|
339
491
|
const contextValue = (0, import_react4.useMemo)(
|
|
340
492
|
() => ({
|
|
341
493
|
editor,
|
|
342
494
|
placeholder,
|
|
495
|
+
editable: !locked,
|
|
343
496
|
editorContainerRef: containerRef
|
|
344
497
|
}),
|
|
345
|
-
[editor, placeholder]
|
|
498
|
+
[editor, placeholder, locked]
|
|
346
499
|
);
|
|
347
500
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
348
501
|
"form",
|
|
349
502
|
{
|
|
503
|
+
...props,
|
|
504
|
+
ref: formRef,
|
|
505
|
+
"data-comment-surface": "composer",
|
|
506
|
+
"aria-busy": isSubmitting || void 0,
|
|
350
507
|
className: (0, import_utils2.cx)(
|
|
351
508
|
"relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
|
|
352
509
|
className
|
|
353
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
|
+
},
|
|
354
526
|
onSubmit: (event) => {
|
|
355
527
|
event.preventDefault();
|
|
356
|
-
if (
|
|
357
|
-
|
|
528
|
+
if (!locked && editor && !readValue(editor).isEmpty)
|
|
529
|
+
onSubmit?.(event);
|
|
358
530
|
},
|
|
359
|
-
...props,
|
|
360
531
|
children: [
|
|
361
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 }) }),
|
|
362
533
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -371,11 +542,11 @@ var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
|
|
|
371
542
|
size: "small",
|
|
372
543
|
isSecondary: true,
|
|
373
544
|
onClick: onCancel,
|
|
374
|
-
disabled:
|
|
545
|
+
disabled: locked,
|
|
375
546
|
children: cancelLabel
|
|
376
547
|
}
|
|
377
548
|
),
|
|
378
|
-
/* @__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 })
|
|
379
550
|
] })
|
|
380
551
|
] })
|
|
381
552
|
] }),
|
|
@@ -383,27 +554,32 @@ var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
|
|
|
383
554
|
]
|
|
384
555
|
}
|
|
385
556
|
) });
|
|
386
|
-
}
|
|
557
|
+
}
|
|
558
|
+
var CommentComposer = (0, import_react4.forwardRef)(CommentComposerComponent);
|
|
387
559
|
|
|
388
560
|
// src/elements/CommentMention.jsx
|
|
389
561
|
var import_react6 = __toESM(require("react"));
|
|
390
562
|
var import_menus = require("@edux-design/menus");
|
|
391
563
|
var import_avatars3 = require("@edux-design/avatars");
|
|
392
564
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
393
|
-
var CommentMention = ({
|
|
394
|
-
|
|
395
|
-
}) => {
|
|
396
|
-
const { editor, editorContainerRef } = useCommentComposer();
|
|
565
|
+
var CommentMention = ({ peopleList = [], onQueryChange }) => {
|
|
566
|
+
const { editor, editorContainerRef, editable = true } = useCommentComposer();
|
|
397
567
|
const [menuOpen, setMenuOpen] = (0, import_react6.useState)(false);
|
|
398
568
|
const [query, setQuery] = (0, import_react6.useState)("");
|
|
399
|
-
const [range, setRange] = (0, import_react6.useState)(null);
|
|
400
569
|
const [anchorStyle, setAnchorStyle] = (0, import_react6.useState)({ left: 0, top: 0 });
|
|
401
570
|
const menuListRef = (0, import_react6.useRef)(null);
|
|
402
571
|
const rangeRef = (0, import_react6.useRef)(null);
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
);
|
|
572
|
+
const lastQueryRef = (0, import_react6.useRef)("");
|
|
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]);
|
|
407
583
|
const filteredPeople = (0, import_react6.useMemo)(() => {
|
|
408
584
|
if (!query) return peopleList;
|
|
409
585
|
const lowered = query.toLowerCase();
|
|
@@ -412,28 +588,32 @@ var CommentMention = ({
|
|
|
412
588
|
);
|
|
413
589
|
}, [peopleList, query]);
|
|
414
590
|
const updateMenuState = (0, import_react6.useCallback)(() => {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
591
|
+
if (!editor || !editable || editor.isEditable === false) {
|
|
592
|
+
setMenuOpen(false);
|
|
593
|
+
rangeRef.current = null;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
418
596
|
const { parent, parentOffset, pos } = editor.state.selection.$from;
|
|
419
597
|
if (!parent.isTextblock) {
|
|
420
598
|
setMenuOpen(false);
|
|
421
599
|
setQuery("");
|
|
422
|
-
setRange(null);
|
|
423
600
|
return;
|
|
424
601
|
}
|
|
425
602
|
const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
|
|
426
603
|
const match = textBefore.match(/@([\w-]*)$/);
|
|
427
604
|
if (match) {
|
|
428
605
|
const handleQuery = match[1] ?? "";
|
|
606
|
+
if (onQueryChange && handleQuery !== lastQueryRef.current) {
|
|
607
|
+
lastQueryRef.current = handleQuery;
|
|
608
|
+
onQueryChange(handleQuery);
|
|
609
|
+
}
|
|
429
610
|
setQuery(handleQuery);
|
|
430
|
-
setMenuOpen(true);
|
|
431
611
|
const fromPos = pos - match[0].length;
|
|
432
612
|
const nextRange = { from: fromPos, to: pos };
|
|
433
|
-
|
|
613
|
+
setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
|
|
434
614
|
rangeRef.current = nextRange;
|
|
435
615
|
const coords = editor.view.coordsAtPos(fromPos);
|
|
436
|
-
const containerRect =
|
|
616
|
+
const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
|
|
437
617
|
if (containerRect) {
|
|
438
618
|
setAnchorStyle({
|
|
439
619
|
left: coords.left - containerRect.left + 12,
|
|
@@ -444,12 +624,16 @@ var CommentMention = ({
|
|
|
444
624
|
}
|
|
445
625
|
setMenuOpen(false);
|
|
446
626
|
setQuery("");
|
|
447
|
-
setRange(null);
|
|
448
627
|
rangeRef.current = null;
|
|
449
|
-
|
|
628
|
+
if (onQueryChange && lastQueryRef.current !== "") {
|
|
629
|
+
lastQueryRef.current = "";
|
|
630
|
+
onQueryChange("");
|
|
631
|
+
}
|
|
632
|
+
}, [editor, editorContainerRef, onQueryChange, editable]);
|
|
450
633
|
const handleSelectPerson = (0, import_react6.useCallback)(
|
|
451
634
|
(person) => {
|
|
452
|
-
if (!editor || !rangeRef.current)
|
|
635
|
+
if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
|
|
636
|
+
return;
|
|
453
637
|
const selectionRange = rangeRef.current;
|
|
454
638
|
editor.commands.focus();
|
|
455
639
|
editor.commands.setTextSelection(selectionRange);
|
|
@@ -469,15 +653,32 @@ var CommentMention = ({
|
|
|
469
653
|
{ updateSelection: false }
|
|
470
654
|
);
|
|
471
655
|
const nextPos = selectionRange.from + 2;
|
|
472
|
-
|
|
656
|
+
editor.view.focus();
|
|
657
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
658
|
+
clearTimeout(focusTimeoutRef.current);
|
|
659
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
660
|
+
if (editor.isDestroyed || editor.isEditable === false) return;
|
|
473
661
|
editor.commands.setTextSelection(nextPos);
|
|
474
662
|
editor.view.focus();
|
|
475
|
-
setTimeout(() =>
|
|
663
|
+
focusTimeoutRef.current = setTimeout(() => {
|
|
664
|
+
if (!editor.isDestroyed && editor.isEditable !== false)
|
|
665
|
+
editor.view.focus();
|
|
666
|
+
}, 0);
|
|
476
667
|
});
|
|
477
668
|
setMenuOpen(false);
|
|
478
669
|
setQuery("");
|
|
479
|
-
setRange(null);
|
|
480
670
|
rangeRef.current = null;
|
|
671
|
+
if (onQueryChange && lastQueryRef.current !== "") {
|
|
672
|
+
lastQueryRef.current = "";
|
|
673
|
+
onQueryChange("");
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
[editor, onQueryChange, editable]
|
|
677
|
+
);
|
|
678
|
+
(0, import_react6.useEffect)(
|
|
679
|
+
() => () => {
|
|
680
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
681
|
+
clearTimeout(focusTimeoutRef.current);
|
|
481
682
|
},
|
|
482
683
|
[editor]
|
|
483
684
|
);
|
|
@@ -492,25 +693,30 @@ var CommentMention = ({
|
|
|
492
693
|
};
|
|
493
694
|
}, [editor, updateMenuState]);
|
|
494
695
|
(0, import_react6.useEffect)(() => {
|
|
495
|
-
var _a;
|
|
496
696
|
if (!editor) return;
|
|
497
697
|
const handleKeyDown = (event) => {
|
|
498
|
-
var _a2;
|
|
499
698
|
if (!menuOpen) return;
|
|
699
|
+
if (event.key === "Escape") {
|
|
700
|
+
event.preventDefault();
|
|
701
|
+
event.stopPropagation();
|
|
702
|
+
dismissMenu();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
500
705
|
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
501
706
|
event.preventDefault();
|
|
707
|
+
event.stopPropagation();
|
|
502
708
|
const list = menuListRef.current;
|
|
503
|
-
const items = list
|
|
504
|
-
if (!list || !
|
|
709
|
+
const items = list?.querySelectorAll("[data-menu-item]");
|
|
710
|
+
if (!list || !items?.length) return;
|
|
505
711
|
list.focus();
|
|
506
712
|
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
507
713
|
const targetIndex = direction === 1 ? 0 : items.length - 1;
|
|
508
|
-
|
|
714
|
+
items[targetIndex]?.focus();
|
|
509
715
|
};
|
|
510
|
-
const dom =
|
|
511
|
-
dom
|
|
512
|
-
return () => dom
|
|
513
|
-
}, [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]);
|
|
514
720
|
const menuContent = (0, import_react6.useMemo)(
|
|
515
721
|
() => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
516
722
|
import_menus.Menu,
|
|
@@ -522,6 +728,7 @@ var CommentMention = ({
|
|
|
522
728
|
{
|
|
523
729
|
type: "button",
|
|
524
730
|
"aria-hidden": true,
|
|
731
|
+
tabIndex: -1,
|
|
525
732
|
className: "absolute h-1 w-1 opacity-0",
|
|
526
733
|
style: {
|
|
527
734
|
left: `${anchorStyle.left}px`,
|
|
@@ -541,32 +748,46 @@ var CommentMention = ({
|
|
|
541
748
|
collisionPadding: 12,
|
|
542
749
|
onCloseAutoFocus: (event) => event.preventDefault()
|
|
543
750
|
},
|
|
544
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
545
|
-
import_menus.
|
|
751
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
752
|
+
import_menus.MenuList,
|
|
546
753
|
{
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
+
}
|
|
551
762
|
},
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
+
] })
|
|
561
782
|
] })
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
)
|
|
783
|
+
},
|
|
784
|
+
person.id
|
|
785
|
+
))
|
|
786
|
+
}
|
|
787
|
+
)
|
|
567
788
|
}
|
|
568
789
|
),
|
|
569
|
-
[filteredPeople, handleSelectPerson, menuOpen]
|
|
790
|
+
[filteredPeople, handleSelectPerson, menuOpen, anchorStyle, dismissMenu]
|
|
570
791
|
);
|
|
571
792
|
if (!editor) return null;
|
|
572
793
|
return menuContent;
|