@edux-design/comments 0.1.1
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 +17 -0
- package/README.md +61 -0
- package/dist/index.d.mts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +579 -0
- package/dist/index.mjs +556 -0
- package/package.json +51 -0
- package/src/demos/Comment.stories.jsx +472 -0
- package/src/demos/CommentComposer.stories.jsx +62 -0
- package/src/elements/Comment.jsx +234 -0
- package/src/elements/Comment.test.jsx +137 -0
- package/src/elements/CommentComposer.jsx +184 -0
- package/src/elements/CommentComposer.test.jsx +125 -0
- package/src/elements/CommentMention.jsx +217 -0
- package/src/elements/CommentMention.test.jsx +138 -0
- package/src/elements/CommentMentionExtension.jsx +56 -0
- package/src/index.js +3 -0
- package/tsconfig.json +10 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/index.js
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
Comment: () => Comment,
|
|
33
|
+
CommentComposer: () => CommentComposer,
|
|
34
|
+
CommentMention: () => CommentMention
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/elements/Comment.jsx
|
|
39
|
+
var import_react = __toESM(require("react"));
|
|
40
|
+
var import_utils = require("@edux-design/utils");
|
|
41
|
+
var import_avatars = require("@edux-design/avatars");
|
|
42
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
43
|
+
var VARIANT_STYLES = {
|
|
44
|
+
default: "border-border-minimal bg-bg-base",
|
|
45
|
+
resolved: "border-border-success-base bg-bg-success-subtle"
|
|
46
|
+
};
|
|
47
|
+
var CommentContext = (0, import_react.createContext)({
|
|
48
|
+
editable: true,
|
|
49
|
+
onResolve: void 0
|
|
50
|
+
});
|
|
51
|
+
var useCommentContext = () => (0, import_react.useContext)(CommentContext);
|
|
52
|
+
var Comment = (0, import_react.forwardRef)(function Comment2({
|
|
53
|
+
author = "Author",
|
|
54
|
+
timestamp,
|
|
55
|
+
avatar,
|
|
56
|
+
avatarName,
|
|
57
|
+
avatarProps,
|
|
58
|
+
body,
|
|
59
|
+
replies,
|
|
60
|
+
variant = "default",
|
|
61
|
+
resolved = false,
|
|
62
|
+
onResolve,
|
|
63
|
+
editable = true,
|
|
64
|
+
children,
|
|
65
|
+
className,
|
|
66
|
+
...props
|
|
67
|
+
}, ref) {
|
|
68
|
+
const childArray = import_react.default.Children.toArray(children);
|
|
69
|
+
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
|
+
);
|
|
75
|
+
const bodyNodes = childArray.filter((child) => child !== actionsSlot);
|
|
76
|
+
const bodyContent = body ?? bodyNodes;
|
|
77
|
+
const hasBodyContent = body !== void 0 ? Boolean(body) : bodyNodes.length > 0;
|
|
78
|
+
const isResolved = resolved || variant === "resolved";
|
|
79
|
+
const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
|
|
80
|
+
const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
|
|
81
|
+
const shouldRenderAvatar = Boolean(avatar || avatarProps || resolvedAvatarName);
|
|
82
|
+
const replyItems = (0, import_react.useMemo)(
|
|
83
|
+
() => import_react.default.Children.toArray(replies).filter(
|
|
84
|
+
(reply) => reply !== null && reply !== void 0 && reply !== false
|
|
85
|
+
),
|
|
86
|
+
[replies]
|
|
87
|
+
);
|
|
88
|
+
const replyCount = replyItems.length;
|
|
89
|
+
const shouldCollapseReplies = replyCount > 2;
|
|
90
|
+
const [showAllReplies, setShowAllReplies] = (0, import_react.useState)(!shouldCollapseReplies);
|
|
91
|
+
(0, import_react.useEffect)(() => {
|
|
92
|
+
setShowAllReplies(!shouldCollapseReplies);
|
|
93
|
+
}, [shouldCollapseReplies]);
|
|
94
|
+
const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
|
|
95
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommentContext.Provider, { value: { editable, onResolve }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
96
|
+
"div",
|
|
97
|
+
{
|
|
98
|
+
ref,
|
|
99
|
+
className: (0, import_utils.cx)(
|
|
100
|
+
"flex gap-12 rounded-default border p-12",
|
|
101
|
+
resolvedVariant,
|
|
102
|
+
className
|
|
103
|
+
),
|
|
104
|
+
"data-variant": variant,
|
|
105
|
+
...props,
|
|
106
|
+
children: [
|
|
107
|
+
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 }) }),
|
|
108
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
109
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-6", children: [
|
|
110
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
|
|
111
|
+
timestamp && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-fg-subtle", children: timestamp })
|
|
112
|
+
] }),
|
|
113
|
+
hasBodyContent && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
|
|
114
|
+
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,
|
|
115
|
+
actionsSlot ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-8", children: actionsSlot }) : null,
|
|
116
|
+
replies && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-10 border-l border-border-minimal pl-12", children: [
|
|
117
|
+
shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
118
|
+
"button",
|
|
119
|
+
{
|
|
120
|
+
type: "button",
|
|
121
|
+
className: "mb-8 text-xs font-semibold text-fg-info-base",
|
|
122
|
+
onClick: () => setShowAllReplies(true),
|
|
123
|
+
"aria-expanded": showAllReplies,
|
|
124
|
+
children: [
|
|
125
|
+
"Show all ",
|
|
126
|
+
replyCount,
|
|
127
|
+
" replies"
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
visibleReplies
|
|
132
|
+
] }) : null
|
|
133
|
+
] })
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
) });
|
|
137
|
+
});
|
|
138
|
+
var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
139
|
+
"div",
|
|
140
|
+
{
|
|
141
|
+
className: (0, import_utils.cx)(
|
|
142
|
+
"flex flex-wrap items-center gap-8 text-xs text-fg-subtle",
|
|
143
|
+
className
|
|
144
|
+
),
|
|
145
|
+
...props,
|
|
146
|
+
children
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
var CommentReply = ({ className, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
150
|
+
"button",
|
|
151
|
+
{
|
|
152
|
+
type: "button",
|
|
153
|
+
className: (0, import_utils.cx)("text-xs text-fg-info-base cursor-pointer", className),
|
|
154
|
+
...props,
|
|
155
|
+
children: "Reply"
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
var CommentEdit = ({ className, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
159
|
+
"button",
|
|
160
|
+
{
|
|
161
|
+
type: "button",
|
|
162
|
+
className: (0, import_utils.cx)("text-xs text-fg-info-base cursor-pointer", className),
|
|
163
|
+
...props,
|
|
164
|
+
children: "Edit"
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
var CommentResolve = ({
|
|
168
|
+
className,
|
|
169
|
+
disabled,
|
|
170
|
+
onClick,
|
|
171
|
+
...props
|
|
172
|
+
}) => {
|
|
173
|
+
const { editable, onResolve } = useCommentContext();
|
|
174
|
+
const isDisabled = disabled || !editable;
|
|
175
|
+
const handleClick = onClick ?? onResolve;
|
|
176
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
177
|
+
"button",
|
|
178
|
+
{
|
|
179
|
+
type: "button",
|
|
180
|
+
className: (0, import_utils.cx)(
|
|
181
|
+
"text-xs font-semibold text-fg-success-base cursor-pointer",
|
|
182
|
+
className
|
|
183
|
+
),
|
|
184
|
+
onClick: handleClick,
|
|
185
|
+
disabled: isDisabled,
|
|
186
|
+
"aria-disabled": isDisabled,
|
|
187
|
+
...props,
|
|
188
|
+
children: "Resolve"
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
Comment.Actions = CommentActions;
|
|
193
|
+
Comment.Reply = CommentReply;
|
|
194
|
+
Comment.Edit = CommentEdit;
|
|
195
|
+
Comment.Resolve = CommentResolve;
|
|
196
|
+
CommentActions.displayName = "Comment.Actions";
|
|
197
|
+
CommentReply.displayName = "Comment.Reply";
|
|
198
|
+
CommentEdit.displayName = "Comment.Edit";
|
|
199
|
+
CommentResolve.displayName = "Comment.Resolve";
|
|
200
|
+
|
|
201
|
+
// src/elements/CommentComposer.jsx
|
|
202
|
+
var import_react4 = __toESM(require("react"));
|
|
203
|
+
var import_utils2 = require("@edux-design/utils");
|
|
204
|
+
var import_avatars2 = require("@edux-design/avatars");
|
|
205
|
+
var import_buttons = require("@edux-design/buttons");
|
|
206
|
+
var import_react5 = require("@tiptap/react");
|
|
207
|
+
var import_starter_kit = __toESM(require("@tiptap/starter-kit"));
|
|
208
|
+
var import_extension_placeholder = __toESM(require("@tiptap/extension-placeholder"));
|
|
209
|
+
|
|
210
|
+
// src/elements/CommentMentionExtension.jsx
|
|
211
|
+
var import_react2 = __toESM(require("react"));
|
|
212
|
+
var import_core = require("@tiptap/core");
|
|
213
|
+
var import_react3 = require("@tiptap/react");
|
|
214
|
+
var import_mentions = require("@edux-design/mentions");
|
|
215
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
216
|
+
var MentionNodeView = ({ node }) => {
|
|
217
|
+
var _a, _b;
|
|
218
|
+
const handle = ((_a = node.attrs) == null ? void 0 : _a.handle) ?? "";
|
|
219
|
+
const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
|
|
220
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
221
|
+
import_react3.NodeViewWrapper,
|
|
222
|
+
{
|
|
223
|
+
as: "span",
|
|
224
|
+
className: "inline-flex items-center align-middle -translate-y-2",
|
|
225
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_mentions.Mention, { handle, size: "small", children: [
|
|
226
|
+
"@",
|
|
227
|
+
handle,
|
|
228
|
+
" \xB7 ",
|
|
229
|
+
label
|
|
230
|
+
] })
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
var CommentMentionExtension = import_core.Node.create({
|
|
235
|
+
name: "commentMention",
|
|
236
|
+
group: "inline",
|
|
237
|
+
inline: true,
|
|
238
|
+
atom: true,
|
|
239
|
+
selectable: false,
|
|
240
|
+
addAttributes() {
|
|
241
|
+
return {
|
|
242
|
+
id: { default: null },
|
|
243
|
+
handle: { default: "" },
|
|
244
|
+
label: { default: "" }
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
parseHTML() {
|
|
248
|
+
return [{ tag: "span[data-comment-mention]" }];
|
|
249
|
+
},
|
|
250
|
+
renderHTML({ HTMLAttributes }) {
|
|
251
|
+
const handle = HTMLAttributes.handle || "";
|
|
252
|
+
const label = HTMLAttributes.label || `@${handle}`;
|
|
253
|
+
return [
|
|
254
|
+
"span",
|
|
255
|
+
(0, import_core.mergeAttributes)(HTMLAttributes, {
|
|
256
|
+
"data-comment-mention": "true",
|
|
257
|
+
"data-handle": handle
|
|
258
|
+
}),
|
|
259
|
+
label
|
|
260
|
+
];
|
|
261
|
+
},
|
|
262
|
+
addNodeView() {
|
|
263
|
+
return (0, import_react3.ReactNodeViewRenderer)(MentionNodeView);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// src/elements/CommentComposer.jsx
|
|
268
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
269
|
+
var CommentComposerContext = (0, import_react4.createContext)(null);
|
|
270
|
+
var useCommentComposer = () => {
|
|
271
|
+
const context = (0, import_react4.useContext)(CommentComposerContext);
|
|
272
|
+
if (!context) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"CommentComposer components must be used within a <CommentComposer />."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return context;
|
|
278
|
+
};
|
|
279
|
+
var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
|
|
280
|
+
value,
|
|
281
|
+
onChange,
|
|
282
|
+
onSubmit,
|
|
283
|
+
onCancel,
|
|
284
|
+
placeholder = "Write a comment...",
|
|
285
|
+
rows = 4,
|
|
286
|
+
disabled = false,
|
|
287
|
+
isSubmitting = false,
|
|
288
|
+
submitLabel = "Post",
|
|
289
|
+
cancelLabel = "Cancel",
|
|
290
|
+
avatar,
|
|
291
|
+
avatarProps,
|
|
292
|
+
actions,
|
|
293
|
+
className,
|
|
294
|
+
children,
|
|
295
|
+
...props
|
|
296
|
+
}, ref) {
|
|
297
|
+
const hasValue = value === void 0 ? true : Boolean(value && value.trim().length);
|
|
298
|
+
const isSubmitDisabled = disabled || isSubmitting || !hasValue;
|
|
299
|
+
const editor = (0, import_react5.useEditor)({
|
|
300
|
+
extensions: [
|
|
301
|
+
import_starter_kit.default,
|
|
302
|
+
CommentMentionExtension,
|
|
303
|
+
import_extension_placeholder.default.configure({
|
|
304
|
+
placeholder
|
|
305
|
+
})
|
|
306
|
+
],
|
|
307
|
+
content: value ?? "",
|
|
308
|
+
editable: !disabled,
|
|
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
|
+
},
|
|
321
|
+
onUpdate: ({ editor: editor2 }) => {
|
|
322
|
+
onChange == null ? void 0 : onChange({ target: { value: editor2.getText() } });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
const containerRef = (0, import_react4.useRef)(null);
|
|
326
|
+
(0, import_react4.useEffect)(() => {
|
|
327
|
+
if (typeof ref === "function") {
|
|
328
|
+
ref(containerRef.current);
|
|
329
|
+
} else if (ref) {
|
|
330
|
+
ref.current = containerRef.current;
|
|
331
|
+
}
|
|
332
|
+
}, [ref]);
|
|
333
|
+
(0, import_react4.useEffect)(() => {
|
|
334
|
+
if (!editor) return;
|
|
335
|
+
if (value === "") {
|
|
336
|
+
editor.commands.clearContent();
|
|
337
|
+
}
|
|
338
|
+
}, [editor, value]);
|
|
339
|
+
const contextValue = (0, import_react4.useMemo)(
|
|
340
|
+
() => ({
|
|
341
|
+
editor,
|
|
342
|
+
placeholder,
|
|
343
|
+
editorContainerRef: containerRef
|
|
344
|
+
}),
|
|
345
|
+
[editor, placeholder]
|
|
346
|
+
);
|
|
347
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
348
|
+
"form",
|
|
349
|
+
{
|
|
350
|
+
className: (0, import_utils2.cx)(
|
|
351
|
+
"relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
|
|
352
|
+
className
|
|
353
|
+
),
|
|
354
|
+
onSubmit: (event) => {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
if (isSubmitDisabled) return;
|
|
357
|
+
onSubmit == null ? void 0 : onSubmit(event);
|
|
358
|
+
},
|
|
359
|
+
...props,
|
|
360
|
+
children: [
|
|
361
|
+
(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
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
363
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: containerRef, className: "relative", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.EditorContent, { editor }) }),
|
|
364
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mt-8 flex flex-wrap items-center justify-between gap-8", children: [
|
|
365
|
+
actions ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex flex-wrap items-center gap-8 text-xs text-fg-subtle", children: actions }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", {}),
|
|
366
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-8", children: [
|
|
367
|
+
onCancel && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
368
|
+
import_buttons.Button,
|
|
369
|
+
{
|
|
370
|
+
type: "button",
|
|
371
|
+
size: "small",
|
|
372
|
+
isSecondary: true,
|
|
373
|
+
onClick: onCancel,
|
|
374
|
+
disabled: disabled || isSubmitting,
|
|
375
|
+
children: cancelLabel
|
|
376
|
+
}
|
|
377
|
+
),
|
|
378
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_buttons.Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? "Posting..." : submitLabel })
|
|
379
|
+
] })
|
|
380
|
+
] })
|
|
381
|
+
] }),
|
|
382
|
+
children
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
) });
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/elements/CommentMention.jsx
|
|
389
|
+
var import_react6 = __toESM(require("react"));
|
|
390
|
+
var import_menus = require("@edux-design/menus");
|
|
391
|
+
var import_avatars3 = require("@edux-design/avatars");
|
|
392
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
393
|
+
var CommentMention = ({
|
|
394
|
+
peopleList = []
|
|
395
|
+
}) => {
|
|
396
|
+
const { editor, editorContainerRef } = useCommentComposer();
|
|
397
|
+
const [menuOpen, setMenuOpen] = (0, import_react6.useState)(false);
|
|
398
|
+
const [query, setQuery] = (0, import_react6.useState)("");
|
|
399
|
+
const [range, setRange] = (0, import_react6.useState)(null);
|
|
400
|
+
const [anchorStyle, setAnchorStyle] = (0, import_react6.useState)({ left: 0, top: 0 });
|
|
401
|
+
const menuListRef = (0, import_react6.useRef)(null);
|
|
402
|
+
const rangeRef = (0, import_react6.useRef)(null);
|
|
403
|
+
const peopleByHandle = (0, import_react6.useMemo)(
|
|
404
|
+
() => new Map(peopleList.map((person) => [person.handle.toLowerCase(), person])),
|
|
405
|
+
[peopleList]
|
|
406
|
+
);
|
|
407
|
+
const filteredPeople = (0, import_react6.useMemo)(() => {
|
|
408
|
+
if (!query) return peopleList;
|
|
409
|
+
const lowered = query.toLowerCase();
|
|
410
|
+
return peopleList.filter(
|
|
411
|
+
(person) => person.handle.toLowerCase().startsWith(lowered)
|
|
412
|
+
);
|
|
413
|
+
}, [peopleList, query]);
|
|
414
|
+
const updateMenuState = (0, import_react6.useCallback)(() => {
|
|
415
|
+
var _a, _b;
|
|
416
|
+
if (!editor) return;
|
|
417
|
+
const { from } = editor.state.selection;
|
|
418
|
+
const { parent, parentOffset, pos } = editor.state.selection.$from;
|
|
419
|
+
if (!parent.isTextblock) {
|
|
420
|
+
setMenuOpen(false);
|
|
421
|
+
setQuery("");
|
|
422
|
+
setRange(null);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
|
|
426
|
+
const match = textBefore.match(/@([\w-]*)$/);
|
|
427
|
+
if (match) {
|
|
428
|
+
const handleQuery = match[1] ?? "";
|
|
429
|
+
setQuery(handleQuery);
|
|
430
|
+
setMenuOpen(true);
|
|
431
|
+
const fromPos = pos - match[0].length;
|
|
432
|
+
const nextRange = { from: fromPos, to: pos };
|
|
433
|
+
setRange(nextRange);
|
|
434
|
+
rangeRef.current = nextRange;
|
|
435
|
+
const coords = editor.view.coordsAtPos(fromPos);
|
|
436
|
+
const containerRect = (_b = (_a = editorContainerRef == null ? void 0 : editorContainerRef.current) == null ? void 0 : _a.getBoundingClientRect) == null ? void 0 : _b.call(_a);
|
|
437
|
+
if (containerRect) {
|
|
438
|
+
setAnchorStyle({
|
|
439
|
+
left: coords.left - containerRect.left + 12,
|
|
440
|
+
top: coords.bottom - containerRect.top + 4
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
setMenuOpen(false);
|
|
446
|
+
setQuery("");
|
|
447
|
+
setRange(null);
|
|
448
|
+
rangeRef.current = null;
|
|
449
|
+
}, [editor]);
|
|
450
|
+
const handleSelectPerson = (0, import_react6.useCallback)(
|
|
451
|
+
(person) => {
|
|
452
|
+
if (!editor || !rangeRef.current) return;
|
|
453
|
+
const selectionRange = rangeRef.current;
|
|
454
|
+
editor.commands.focus();
|
|
455
|
+
editor.commands.setTextSelection(selectionRange);
|
|
456
|
+
editor.commands.insertContentAt(
|
|
457
|
+
selectionRange,
|
|
458
|
+
[
|
|
459
|
+
{
|
|
460
|
+
type: "commentMention",
|
|
461
|
+
attrs: {
|
|
462
|
+
id: person.id,
|
|
463
|
+
handle: person.handle,
|
|
464
|
+
label: person.name
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
{ type: "text", text: " " }
|
|
468
|
+
],
|
|
469
|
+
{ updateSelection: false }
|
|
470
|
+
);
|
|
471
|
+
const nextPos = selectionRange.from + 2;
|
|
472
|
+
requestAnimationFrame(() => {
|
|
473
|
+
editor.commands.setTextSelection(nextPos);
|
|
474
|
+
editor.view.focus();
|
|
475
|
+
setTimeout(() => editor.view.focus(), 0);
|
|
476
|
+
});
|
|
477
|
+
setMenuOpen(false);
|
|
478
|
+
setQuery("");
|
|
479
|
+
setRange(null);
|
|
480
|
+
rangeRef.current = null;
|
|
481
|
+
},
|
|
482
|
+
[editor]
|
|
483
|
+
);
|
|
484
|
+
(0, import_react6.useEffect)(() => {
|
|
485
|
+
if (!editor) return;
|
|
486
|
+
updateMenuState();
|
|
487
|
+
editor.on("transaction", updateMenuState);
|
|
488
|
+
editor.on("selectionUpdate", updateMenuState);
|
|
489
|
+
return () => {
|
|
490
|
+
editor.off("transaction", updateMenuState);
|
|
491
|
+
editor.off("selectionUpdate", updateMenuState);
|
|
492
|
+
};
|
|
493
|
+
}, [editor, updateMenuState]);
|
|
494
|
+
(0, import_react6.useEffect)(() => {
|
|
495
|
+
var _a;
|
|
496
|
+
if (!editor) return;
|
|
497
|
+
const handleKeyDown = (event) => {
|
|
498
|
+
var _a2;
|
|
499
|
+
if (!menuOpen) return;
|
|
500
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
501
|
+
event.preventDefault();
|
|
502
|
+
const list = menuListRef.current;
|
|
503
|
+
const items = list == null ? void 0 : list.querySelectorAll("[data-menu-item]");
|
|
504
|
+
if (!list || !(items == null ? void 0 : items.length)) return;
|
|
505
|
+
list.focus();
|
|
506
|
+
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
507
|
+
const targetIndex = direction === 1 ? 0 : items.length - 1;
|
|
508
|
+
(_a2 = items[targetIndex]) == null ? void 0 : _a2.focus();
|
|
509
|
+
};
|
|
510
|
+
const dom = (_a = editor.view) == null ? void 0 : _a.dom;
|
|
511
|
+
dom == null ? void 0 : dom.addEventListener("keydown", handleKeyDown);
|
|
512
|
+
return () => dom == null ? void 0 : dom.removeEventListener("keydown", handleKeyDown);
|
|
513
|
+
}, [editor, menuOpen]);
|
|
514
|
+
const menuContent = (0, import_react6.useMemo)(
|
|
515
|
+
() => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
516
|
+
import_menus.Menu,
|
|
517
|
+
{
|
|
518
|
+
open: menuOpen && filteredPeople.length > 0,
|
|
519
|
+
onOpenChange: setMenuOpen,
|
|
520
|
+
button: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
521
|
+
"button",
|
|
522
|
+
{
|
|
523
|
+
type: "button",
|
|
524
|
+
"aria-hidden": true,
|
|
525
|
+
className: "absolute h-1 w-1 opacity-0",
|
|
526
|
+
style: {
|
|
527
|
+
left: `${anchorStyle.left}px`,
|
|
528
|
+
top: `${anchorStyle.top}px`,
|
|
529
|
+
pointerEvents: "none"
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
),
|
|
533
|
+
align: "start",
|
|
534
|
+
side: "bottom",
|
|
535
|
+
trapFocus: false,
|
|
536
|
+
disableOutsidePointerEvents: false,
|
|
537
|
+
onOpenAutoFocus: (event) => event.preventDefault(),
|
|
538
|
+
contentProps: {
|
|
539
|
+
sideOffset: 4,
|
|
540
|
+
avoidCollisions: true,
|
|
541
|
+
collisionPadding: 12,
|
|
542
|
+
onCloseAutoFocus: (event) => event.preventDefault()
|
|
543
|
+
},
|
|
544
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_menus.MenuList, { ref: menuListRef, className: "min-w-[220px]", children: filteredPeople.map((person) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
545
|
+
import_menus.MenuItem,
|
|
546
|
+
{
|
|
547
|
+
onMouseDown: (event) => {
|
|
548
|
+
event.preventDefault();
|
|
549
|
+
event.stopPropagation();
|
|
550
|
+
handleSelectPerson(person);
|
|
551
|
+
},
|
|
552
|
+
onClick: () => handleSelectPerson(person),
|
|
553
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
554
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-8", children: [
|
|
555
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_avatars3.Avatar, { size: "small", ...person.avatarProps }),
|
|
556
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex flex-col", children: [
|
|
557
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm text-fg-base", children: person.name }),
|
|
558
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-xs text-fg-subtle", children: [
|
|
559
|
+
"@",
|
|
560
|
+
person.handle
|
|
561
|
+
] })
|
|
562
|
+
] })
|
|
563
|
+
] })
|
|
564
|
+
},
|
|
565
|
+
person.id
|
|
566
|
+
)) })
|
|
567
|
+
}
|
|
568
|
+
),
|
|
569
|
+
[filteredPeople, handleSelectPerson, menuOpen]
|
|
570
|
+
);
|
|
571
|
+
if (!editor) return null;
|
|
572
|
+
return menuContent;
|
|
573
|
+
};
|
|
574
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
575
|
+
0 && (module.exports = {
|
|
576
|
+
Comment,
|
|
577
|
+
CommentComposer,
|
|
578
|
+
CommentMention
|
|
579
|
+
});
|