@mrsf/marp-mrsf 0.4.8
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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +317 -0
- package/dist/browser.js.map +7 -0
- package/dist/controller.d.ts +173 -0
- package/dist/controller.js +1179 -0
- package/dist/controller.js.map +7 -0
- package/dist/gutter.d.ts +50 -0
- package/dist/html.d.ts +13 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +353 -0
- package/dist/index.js.map +7 -0
- package/dist/rules/core.d.ts +17 -0
- package/dist/rules/renderer.d.ts +10 -0
- package/dist/shared.d.ts +20 -0
- package/dist/style.css +522 -0
- package/dist/types.d.ts +105 -0
- package/package.json +85 -0
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
// src/gutter.ts
|
|
2
|
+
function formatMrsfCount(count, max = 9) {
|
|
3
|
+
return count > max ? `${max}+` : String(count);
|
|
4
|
+
}
|
|
5
|
+
function createMrsfGutterBadgePresentation(context) {
|
|
6
|
+
const icon = context.resolvedState === "resolved" ? "\u2713" : "\u{1F4AC}";
|
|
7
|
+
const countText = formatMrsfCount(context.commentCount);
|
|
8
|
+
const label = `${icon} ${countText}`;
|
|
9
|
+
const threadSummary = context.threadCount === 1 ? "1 thread" : `${context.threadCount} threads`;
|
|
10
|
+
const commentSummary = context.commentCount === 1 ? "1 comment" : `${context.commentCount} comments`;
|
|
11
|
+
return {
|
|
12
|
+
icon,
|
|
13
|
+
countText,
|
|
14
|
+
label,
|
|
15
|
+
title: `${threadSummary}, ${commentSummary}`,
|
|
16
|
+
ariaLabel: `${label} on line ${context.line}`
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function createMrsfGutterAddButtonPresentation(_context) {
|
|
20
|
+
return {
|
|
21
|
+
label: "Add",
|
|
22
|
+
title: "Add comment thread",
|
|
23
|
+
ariaLabel: "Add comment thread"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function resolveMrsfGutterBadgePresentation(context, renderer) {
|
|
27
|
+
const defaultPresentation = createMrsfGutterBadgePresentation(context);
|
|
28
|
+
const override = renderer?.({
|
|
29
|
+
...context,
|
|
30
|
+
defaultPresentation
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
...defaultPresentation,
|
|
34
|
+
...override,
|
|
35
|
+
attributes: {
|
|
36
|
+
...defaultPresentation.attributes ?? {},
|
|
37
|
+
...override?.attributes ?? {}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function resolveMrsfGutterAddButtonPresentation(context, renderer) {
|
|
42
|
+
const defaultPresentation = createMrsfGutterAddButtonPresentation(context);
|
|
43
|
+
const override = renderer?.({
|
|
44
|
+
...context,
|
|
45
|
+
defaultPresentation
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
...defaultPresentation,
|
|
49
|
+
...override,
|
|
50
|
+
attributes: {
|
|
51
|
+
...defaultPresentation.attributes ?? {},
|
|
52
|
+
...override?.attributes ?? {}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/html.ts
|
|
58
|
+
function escapeHtml(str) {
|
|
59
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
60
|
+
}
|
|
61
|
+
function formatTime(iso) {
|
|
62
|
+
if (!iso) return "";
|
|
63
|
+
try {
|
|
64
|
+
const d = new Date(iso);
|
|
65
|
+
return d.toLocaleDateString(void 0, {
|
|
66
|
+
year: "numeric",
|
|
67
|
+
month: "short",
|
|
68
|
+
day: "numeric"
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function renderCommentHtml(comment, isReply, interactive) {
|
|
75
|
+
const resolvedClass = comment.resolved ? " mrsf-resolved" : "";
|
|
76
|
+
const replyClass = isReply ? " mrsf-reply" : "";
|
|
77
|
+
let html = `<div class="mrsf-comment${resolvedClass}${replyClass}" data-mrsf-comment-id="${escapeHtml(comment.id)}">`;
|
|
78
|
+
html += `<div class="mrsf-comment-header">`;
|
|
79
|
+
html += `<span class="mrsf-author">${escapeHtml(comment.author)}</span>`;
|
|
80
|
+
if (comment.timestamp) {
|
|
81
|
+
html += `<span class="mrsf-date">${escapeHtml(formatTime(comment.timestamp))}</span>`;
|
|
82
|
+
}
|
|
83
|
+
if (comment.severity) {
|
|
84
|
+
html += `<span class="mrsf-severity mrsf-severity-${escapeHtml(comment.severity)}">${escapeHtml(comment.severity)}</span>`;
|
|
85
|
+
}
|
|
86
|
+
if (comment.type) {
|
|
87
|
+
html += `<span class="mrsf-type">${escapeHtml(comment.type)}</span>`;
|
|
88
|
+
}
|
|
89
|
+
if (comment.resolved) {
|
|
90
|
+
html += `<span class="mrsf-resolved-badge">\u2713 resolved</span>`;
|
|
91
|
+
}
|
|
92
|
+
html += `</div>`;
|
|
93
|
+
if (comment.selected_text) {
|
|
94
|
+
html += `<details class="mrsf-selected-text"><summary class="mrsf-selected-text-summary">${escapeHtml(comment.selected_text)}</summary><div class="mrsf-selected-text-full">${escapeHtml(comment.selected_text)}</div></details>`;
|
|
95
|
+
}
|
|
96
|
+
html += `<div class="mrsf-comment-body">${escapeHtml(comment.text)}</div>`;
|
|
97
|
+
if (interactive) {
|
|
98
|
+
const line = comment.line != null ? String(comment.line) : "";
|
|
99
|
+
html += `<div class="mrsf-actions">`;
|
|
100
|
+
if (comment.resolved) {
|
|
101
|
+
html += `<button class="mrsf-action-btn" data-mrsf-action="unresolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Unresolve</button>`;
|
|
102
|
+
} else {
|
|
103
|
+
html += `<button class="mrsf-action-btn" data-mrsf-action="resolve" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Resolve</button>`;
|
|
104
|
+
}
|
|
105
|
+
html += `<button class="mrsf-action-btn" data-mrsf-action="reply" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Reply</button>`;
|
|
106
|
+
html += `<button class="mrsf-action-btn" data-mrsf-action="edit" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Edit</button>`;
|
|
107
|
+
html += `<button class="mrsf-action-btn mrsf-action-danger" data-mrsf-action="delete" data-mrsf-comment-id="${escapeHtml(comment.id)}" data-mrsf-line="${line}">Delete</button>`;
|
|
108
|
+
html += `</div>`;
|
|
109
|
+
}
|
|
110
|
+
html += `</div>`;
|
|
111
|
+
return html;
|
|
112
|
+
}
|
|
113
|
+
function renderThreadHtml(thread, interactive) {
|
|
114
|
+
let html = `<div class="mrsf-thread">`;
|
|
115
|
+
html += renderCommentHtml(thread.comment, false, interactive);
|
|
116
|
+
if (thread.replies.length > 0) {
|
|
117
|
+
html += `<div class="mrsf-replies">`;
|
|
118
|
+
for (const reply of thread.replies) {
|
|
119
|
+
html += renderCommentHtml(reply, true, interactive);
|
|
120
|
+
}
|
|
121
|
+
html += `</div>`;
|
|
122
|
+
}
|
|
123
|
+
html += `</div>`;
|
|
124
|
+
return html;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/controller.ts
|
|
128
|
+
var MrsfController = class _MrsfController {
|
|
129
|
+
container;
|
|
130
|
+
opts;
|
|
131
|
+
threads = /* @__PURE__ */ new Map();
|
|
132
|
+
gutterLeft = null;
|
|
133
|
+
gutterRight = null;
|
|
134
|
+
activeTooltip = null;
|
|
135
|
+
floatingAddButton = null;
|
|
136
|
+
overlayEl = null;
|
|
137
|
+
lastSelectionText = null;
|
|
138
|
+
resizeObserver = null;
|
|
139
|
+
mutationObserver = null;
|
|
140
|
+
styleInjected = false;
|
|
141
|
+
inlineMarks = [];
|
|
142
|
+
inlineTooltipEl = null;
|
|
143
|
+
orphanedSection = null;
|
|
144
|
+
refreshQueued = false;
|
|
145
|
+
handleResizeBound = this.positionGutterItems.bind(this);
|
|
146
|
+
handleMutationBound = this.handleMutations.bind(this);
|
|
147
|
+
handleClickBound = this.handleClick.bind(this);
|
|
148
|
+
handleSelectionBound = this.handleSelectionChange.bind(this);
|
|
149
|
+
constructor(container, options = {}) {
|
|
150
|
+
this.container = container;
|
|
151
|
+
this.opts = {
|
|
152
|
+
gutterPosition: options.gutterPosition ?? "right",
|
|
153
|
+
interactive: options.interactive ?? false,
|
|
154
|
+
comments: options.comments ?? [],
|
|
155
|
+
inlineHighlights: options.inlineHighlights ?? true,
|
|
156
|
+
gutterRenderers: options.gutterRenderers ?? {}
|
|
157
|
+
};
|
|
158
|
+
this.loadCommentData();
|
|
159
|
+
this.setupOverlayStructure();
|
|
160
|
+
this.renderGutterItems();
|
|
161
|
+
this.positionGutterItems();
|
|
162
|
+
this.renderInlineHighlights();
|
|
163
|
+
this.renderOrphanedSection();
|
|
164
|
+
controllerRegistry.add(this);
|
|
165
|
+
this.resizeObserver = new ResizeObserver(this.handleResizeBound);
|
|
166
|
+
this.resizeObserver.observe(this.container);
|
|
167
|
+
if (typeof MutationObserver !== "undefined") {
|
|
168
|
+
this.mutationObserver = new MutationObserver(this.handleMutationBound);
|
|
169
|
+
this.mutationObserver.observe(this.container, {
|
|
170
|
+
childList: true,
|
|
171
|
+
subtree: true,
|
|
172
|
+
characterData: true
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
document.addEventListener("click", this.handleClickBound);
|
|
176
|
+
if (this.opts.interactive) {
|
|
177
|
+
document.addEventListener("selectionchange", this.handleSelectionBound);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Remove all controller DOM and listeners. */
|
|
181
|
+
destroy() {
|
|
182
|
+
this.resizeObserver?.disconnect();
|
|
183
|
+
this.mutationObserver?.disconnect();
|
|
184
|
+
controllerRegistry.delete(this);
|
|
185
|
+
document.removeEventListener("click", this.handleClickBound);
|
|
186
|
+
document.removeEventListener("selectionchange", this.handleSelectionBound);
|
|
187
|
+
this.removeInlineHighlights();
|
|
188
|
+
this.orphanedSection?.remove();
|
|
189
|
+
this.gutterLeft?.remove();
|
|
190
|
+
this.gutterRight?.remove();
|
|
191
|
+
this.floatingAddButton?.remove();
|
|
192
|
+
this.closeOverlay();
|
|
193
|
+
this.container.classList.remove("mrsf-overlay-root");
|
|
194
|
+
}
|
|
195
|
+
/** Recalculate gutter positions after async layout changes such as Mermaid renders. */
|
|
196
|
+
refresh() {
|
|
197
|
+
this.positionGutterItems();
|
|
198
|
+
}
|
|
199
|
+
// ── Data loading ────────────────────────────────────────
|
|
200
|
+
loadCommentData() {
|
|
201
|
+
if (this.opts.comments.length > 0) {
|
|
202
|
+
this.buildThreadMap(this.opts.comments);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const script = this.container.querySelector('script[type="application/mrsf+json"]');
|
|
206
|
+
if (!script?.textContent) return;
|
|
207
|
+
try {
|
|
208
|
+
const data = JSON.parse(script.textContent);
|
|
209
|
+
if (data.threads) {
|
|
210
|
+
this.buildThreadMap(data.threads);
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
buildThreadMap(threads) {
|
|
216
|
+
this.threads.clear();
|
|
217
|
+
for (const t of threads) {
|
|
218
|
+
const line = t.comment.line;
|
|
219
|
+
if (line == null) continue;
|
|
220
|
+
const arr = this.threads.get(line) ?? [];
|
|
221
|
+
arr.push(t);
|
|
222
|
+
this.threads.set(line, arr);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
findCommentById(commentId) {
|
|
226
|
+
if (!commentId) return null;
|
|
227
|
+
for (const threadList of this.threads.values()) {
|
|
228
|
+
for (const thread of threadList) {
|
|
229
|
+
if (thread.comment.id === commentId) {
|
|
230
|
+
return thread.comment;
|
|
231
|
+
}
|
|
232
|
+
const reply = thread.replies.find((item) => item.id === commentId);
|
|
233
|
+
if (reply) {
|
|
234
|
+
return reply;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
orderThreadsForDisplay(threads) {
|
|
241
|
+
return [...threads].sort((left, right) => {
|
|
242
|
+
if (left.comment.resolved === right.comment.resolved) return 0;
|
|
243
|
+
return left.comment.resolved ? 1 : -1;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// ── Overlay structure ───────────────────────────────────
|
|
247
|
+
setupOverlayStructure() {
|
|
248
|
+
this.container.classList.add("mrsf-overlay-root");
|
|
249
|
+
const pos = this.opts.gutterPosition;
|
|
250
|
+
if (pos === "left") {
|
|
251
|
+
this.gutterLeft = this.createGutter("mrsf-gutter-left");
|
|
252
|
+
} else {
|
|
253
|
+
this.gutterRight = this.createGutter("mrsf-gutter-right");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
createGutter(cls) {
|
|
257
|
+
const gutter = document.createElement("div");
|
|
258
|
+
gutter.className = `mrsf-gutter ${cls}`;
|
|
259
|
+
this.container.appendChild(gutter);
|
|
260
|
+
return gutter;
|
|
261
|
+
}
|
|
262
|
+
// ── Gutter rendering ───────────────────────────────────
|
|
263
|
+
/** Build badge/add-button elements for each line in the gutter(s). */
|
|
264
|
+
renderGutterItems() {
|
|
265
|
+
const lines = this.collectLines();
|
|
266
|
+
const displayMap = this.collectThreadDisplayMap(lines);
|
|
267
|
+
const gutter = this.primaryGutter();
|
|
268
|
+
if (!gutter) return;
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
const threads = displayMap.get(line);
|
|
271
|
+
if (threads && threads.length > 0) {
|
|
272
|
+
const item = this.createBadgeItem(line, threads);
|
|
273
|
+
gutter.appendChild(item);
|
|
274
|
+
} else if (this.opts.interactive) {
|
|
275
|
+
const item = this.createAddItem(line);
|
|
276
|
+
gutter.appendChild(item);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
primaryGutter() {
|
|
281
|
+
return this.gutterLeft ?? this.gutterRight;
|
|
282
|
+
}
|
|
283
|
+
shouldExpandRange(el) {
|
|
284
|
+
return el.tagName === "BLOCKQUOTE" || el.tagName === "PRE";
|
|
285
|
+
}
|
|
286
|
+
addVisibleLinesForElement(el, seen) {
|
|
287
|
+
const line = parseInt(el.dataset.mrsfLine ?? "", 10);
|
|
288
|
+
const startLine = parseInt(el.dataset.mrsfStartLine ?? "", 10);
|
|
289
|
+
const endLine = parseInt(el.dataset.mrsfEndLine ?? "", 10);
|
|
290
|
+
if (el.tagName === "PRE" && !isNaN(startLine) && !isNaN(endLine) && endLine > startLine) {
|
|
291
|
+
const visibleStart = startLine + 1;
|
|
292
|
+
const visibleEnd = endLine - 1;
|
|
293
|
+
for (let currentLine = visibleStart; currentLine <= visibleEnd; currentLine++) {
|
|
294
|
+
seen.add(currentLine);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!isNaN(line)) {
|
|
299
|
+
seen.add(line);
|
|
300
|
+
}
|
|
301
|
+
if (this.shouldExpandRange(el) && !isNaN(startLine) && !isNaN(endLine) && endLine > startLine) {
|
|
302
|
+
for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
|
|
303
|
+
seen.add(currentLine);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Collect all unique line numbers from data-mrsf-line elements, expanding multi-line ranges. */
|
|
308
|
+
collectLines() {
|
|
309
|
+
const els = this.container.querySelectorAll("[data-mrsf-line]");
|
|
310
|
+
const seen = /* @__PURE__ */ new Set();
|
|
311
|
+
for (const el of els) {
|
|
312
|
+
if (el.tagName === "SCRIPT") continue;
|
|
313
|
+
this.addVisibleLinesForElement(el, seen);
|
|
314
|
+
}
|
|
315
|
+
return [...seen].sort((a, b) => a - b);
|
|
316
|
+
}
|
|
317
|
+
resolveThreadDisplayLine(thread, availableLines) {
|
|
318
|
+
const startLine = thread.comment.line;
|
|
319
|
+
if (startLine == null) return null;
|
|
320
|
+
const endLine = thread.comment.end_line != null && thread.comment.end_line >= startLine ? thread.comment.end_line : startLine;
|
|
321
|
+
for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
|
|
322
|
+
if (availableLines.has(currentLine)) {
|
|
323
|
+
return currentLine;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
collectThreadDisplayMap(lines) {
|
|
329
|
+
const availableLines = new Set(lines);
|
|
330
|
+
const displayMap = /* @__PURE__ */ new Map();
|
|
331
|
+
for (const threads of this.threads.values()) {
|
|
332
|
+
for (const thread of threads) {
|
|
333
|
+
const displayLine = this.resolveThreadDisplayLine(thread, availableLines);
|
|
334
|
+
if (displayLine == null) continue;
|
|
335
|
+
const existing = displayMap.get(displayLine) ?? [];
|
|
336
|
+
existing.push(thread);
|
|
337
|
+
displayMap.set(displayLine, existing);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return displayMap;
|
|
341
|
+
}
|
|
342
|
+
createBadgeItem(line, threads) {
|
|
343
|
+
const item = document.createElement("div");
|
|
344
|
+
item.className = "mrsf-gutter-item";
|
|
345
|
+
item.dataset.mrsfGutterLine = String(line);
|
|
346
|
+
const displayThreads = this.orderThreadsForDisplay(threads);
|
|
347
|
+
const total = threads.reduce((n, t) => n + 1 + t.replies.length, 0);
|
|
348
|
+
const allResolved = threads.every((t) => t.comment.resolved);
|
|
349
|
+
const highestSeverity = threads.reduce((sev, t) => {
|
|
350
|
+
if (t.comment.severity === "high" || sev === "high") return "high";
|
|
351
|
+
if (t.comment.severity === "medium" || sev === "medium") return "medium";
|
|
352
|
+
if (t.comment.severity === "low" || sev === "low") return "low";
|
|
353
|
+
return sev;
|
|
354
|
+
}, null);
|
|
355
|
+
const classes = ["mrsf-badge"];
|
|
356
|
+
if (allResolved) classes.push("mrsf-badge-resolved");
|
|
357
|
+
if (highestSeverity === "high" || highestSeverity === "medium") {
|
|
358
|
+
classes.push(`mrsf-badge-severity-${highestSeverity}`);
|
|
359
|
+
}
|
|
360
|
+
const presentation = resolveMrsfGutterBadgePresentation(
|
|
361
|
+
{
|
|
362
|
+
line,
|
|
363
|
+
commentCount: total,
|
|
364
|
+
threadCount: threads.length,
|
|
365
|
+
resolvedState: allResolved ? "resolved" : threads.some((thread) => thread.comment.resolved) ? "mixed" : "open",
|
|
366
|
+
highestSeverity,
|
|
367
|
+
isActive: this.activeTooltip?.parentElement === item
|
|
368
|
+
},
|
|
369
|
+
this.opts.gutterRenderers.badge
|
|
370
|
+
);
|
|
371
|
+
const badge = document.createElement("span");
|
|
372
|
+
badge.className = classes.join(" ");
|
|
373
|
+
if (presentation.className) {
|
|
374
|
+
badge.classList.add(...presentation.className.split(/\s+/).filter(Boolean));
|
|
375
|
+
}
|
|
376
|
+
badge.dataset.mrsfLine = String(line);
|
|
377
|
+
badge.dataset.mrsfAction = "navigate";
|
|
378
|
+
badge.dataset.mrsfCommentId = displayThreads[0].comment.id;
|
|
379
|
+
badge.tabIndex = 0;
|
|
380
|
+
badge.textContent = presentation.label;
|
|
381
|
+
badge.title = presentation.title;
|
|
382
|
+
badge.setAttribute("aria-label", presentation.ariaLabel);
|
|
383
|
+
for (const [name, value] of Object.entries(presentation.attributes ?? {})) {
|
|
384
|
+
badge.setAttribute(name, value);
|
|
385
|
+
}
|
|
386
|
+
badge.addEventListener("click", (e) => {
|
|
387
|
+
e.stopPropagation();
|
|
388
|
+
this.toggleTooltip(item, line, displayThreads);
|
|
389
|
+
});
|
|
390
|
+
item.appendChild(badge);
|
|
391
|
+
return item;
|
|
392
|
+
}
|
|
393
|
+
createAddItem(line) {
|
|
394
|
+
const item = document.createElement("div");
|
|
395
|
+
item.className = "mrsf-gutter-item";
|
|
396
|
+
item.dataset.mrsfGutterLine = String(line);
|
|
397
|
+
const addBtn = this.createAddButton(line);
|
|
398
|
+
item.appendChild(addBtn);
|
|
399
|
+
return item;
|
|
400
|
+
}
|
|
401
|
+
createAddButton(line) {
|
|
402
|
+
const presentation = resolveMrsfGutterAddButtonPresentation(
|
|
403
|
+
{
|
|
404
|
+
line,
|
|
405
|
+
isActive: false
|
|
406
|
+
},
|
|
407
|
+
this.opts.gutterRenderers.addButton
|
|
408
|
+
);
|
|
409
|
+
const btn = document.createElement("button");
|
|
410
|
+
btn.className = "mrsf-gutter-add";
|
|
411
|
+
if (presentation.className) {
|
|
412
|
+
btn.classList.add(...presentation.className.split(/\s+/).filter(Boolean));
|
|
413
|
+
}
|
|
414
|
+
btn.type = "button";
|
|
415
|
+
btn.dataset.mrsfAction = "add";
|
|
416
|
+
btn.dataset.mrsfLine = String(line);
|
|
417
|
+
btn.dataset.mrsfStartLine = String(line);
|
|
418
|
+
btn.dataset.mrsfEndLine = String(line);
|
|
419
|
+
btn.title = presentation.title;
|
|
420
|
+
btn.setAttribute("aria-label", presentation.ariaLabel);
|
|
421
|
+
btn.textContent = presentation.label;
|
|
422
|
+
for (const [name, value] of Object.entries(presentation.attributes ?? {})) {
|
|
423
|
+
btn.setAttribute(name, value);
|
|
424
|
+
}
|
|
425
|
+
return btn;
|
|
426
|
+
}
|
|
427
|
+
// ── Positioning ─────────────────────────────────────────
|
|
428
|
+
/** Measure [data-mrsf-line] elements and set gutter item Y offsets. */
|
|
429
|
+
positionGutterItems() {
|
|
430
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
431
|
+
const gutters = [this.gutterLeft, this.gutterRight].filter(Boolean);
|
|
432
|
+
for (const gutter of gutters) {
|
|
433
|
+
const items = gutter.querySelectorAll(".mrsf-gutter-item");
|
|
434
|
+
const positioned = [];
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
const line = parseInt(item.dataset.mrsfGutterLine, 10);
|
|
437
|
+
let target = this.findDirectElementForLine(line);
|
|
438
|
+
if (!target) {
|
|
439
|
+
target = this.findElementForLine(line);
|
|
440
|
+
}
|
|
441
|
+
if (!target) {
|
|
442
|
+
item.style.display = "none";
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const top = this.calculateItemTop(target, line, containerRect);
|
|
446
|
+
item.style.top = `${top}px`;
|
|
447
|
+
item.style.display = "";
|
|
448
|
+
positioned.push({
|
|
449
|
+
item,
|
|
450
|
+
target,
|
|
451
|
+
kind: item.querySelector(".mrsf-badge") ? "badge" : "add"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
this.suppressAddItemsThatShareBadgeTargets(positioned);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
suppressAddItemsThatShareBadgeTargets(positioned) {
|
|
458
|
+
const badgeTargets = new Set(
|
|
459
|
+
positioned.filter((entry) => entry.kind === "badge").map((entry) => entry.target)
|
|
460
|
+
);
|
|
461
|
+
for (const entry of positioned) {
|
|
462
|
+
if (entry.kind === "add" && badgeTargets.has(entry.target)) {
|
|
463
|
+
entry.item.style.display = "none";
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
handleMutations(records) {
|
|
468
|
+
if (!records.some((record) => this.isExternalContentMutation(record))) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.queueRefresh();
|
|
472
|
+
}
|
|
473
|
+
isExternalContentMutation(record) {
|
|
474
|
+
const target = record.target instanceof Node ? record.target : null;
|
|
475
|
+
if (target && this.isControllerOwnedNode(target)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
for (const node of [...record.addedNodes, ...record.removedNodes]) {
|
|
479
|
+
if (!this.isControllerOwnedNode(node)) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return record.type === "characterData";
|
|
484
|
+
}
|
|
485
|
+
isControllerOwnedNode(node) {
|
|
486
|
+
if (!(node instanceof Element)) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
return Boolean(
|
|
490
|
+
node.closest(".mrsf-gutter") || node.closest(".mrsf-orphaned-section") || node.closest('script[type="application/mrsf+json"]')
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
queueRefresh() {
|
|
494
|
+
if (this.refreshQueued) return;
|
|
495
|
+
this.refreshQueued = true;
|
|
496
|
+
queueMicrotask(() => {
|
|
497
|
+
this.refreshQueued = false;
|
|
498
|
+
this.refresh();
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
findDirectElementForLine(line) {
|
|
502
|
+
const els = this.container.querySelectorAll(`[data-mrsf-line="${line}"]`);
|
|
503
|
+
for (const el of els) {
|
|
504
|
+
if (el.tagName === "SCRIPT") continue;
|
|
505
|
+
if (el.closest(".mrsf-gutter")) continue;
|
|
506
|
+
if (el.classList.contains("mrsf-gutter-item")) continue;
|
|
507
|
+
return el;
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
calculateItemTop(target, line, containerRect) {
|
|
512
|
+
const targetRect = target.getBoundingClientRect();
|
|
513
|
+
const rangeTop = targetRect.top - containerRect.top + this.container.scrollTop;
|
|
514
|
+
if (target.tagName !== "PRE") {
|
|
515
|
+
return rangeTop;
|
|
516
|
+
}
|
|
517
|
+
const startLine = parseInt(target.dataset.mrsfStartLine ?? "", 10);
|
|
518
|
+
const endLine = parseInt(target.dataset.mrsfEndLine ?? "", 10);
|
|
519
|
+
const visibleStart = startLine + 1;
|
|
520
|
+
const visibleEnd = endLine - 1;
|
|
521
|
+
const visibleLineCount = visibleEnd - visibleStart + 1;
|
|
522
|
+
if (isNaN(startLine) || isNaN(endLine) || visibleLineCount <= 0 || line < visibleStart || line > visibleEnd) {
|
|
523
|
+
return rangeTop;
|
|
524
|
+
}
|
|
525
|
+
const styles = window.getComputedStyle(target);
|
|
526
|
+
const paddingTop = parseFloat(styles.paddingTop) || 0;
|
|
527
|
+
const paddingBottom = parseFloat(styles.paddingBottom) || 0;
|
|
528
|
+
const contentHeight = Math.max(targetRect.height - paddingTop - paddingBottom, 0);
|
|
529
|
+
let lineHeight = parseFloat(styles.lineHeight);
|
|
530
|
+
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
531
|
+
lineHeight = visibleLineCount > 0 ? contentHeight / visibleLineCount : 0;
|
|
532
|
+
}
|
|
533
|
+
return rangeTop + paddingTop + (line - visibleStart) * lineHeight;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Find the element whose start-line/end-line range contains the given line.
|
|
537
|
+
* Used for positioning gutter items on expanded multi-line elements.
|
|
538
|
+
*/
|
|
539
|
+
findElementForLine(line) {
|
|
540
|
+
const els = this.container.querySelectorAll("[data-mrsf-start-line][data-mrsf-end-line]");
|
|
541
|
+
for (const el of els) {
|
|
542
|
+
if (el.tagName === "SCRIPT") continue;
|
|
543
|
+
const start = parseInt(el.dataset.mrsfStartLine, 10);
|
|
544
|
+
const end = parseInt(el.dataset.mrsfEndLine, 10);
|
|
545
|
+
if (!isNaN(start) && !isNaN(end) && line >= start && line <= end) {
|
|
546
|
+
return el;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
// ── Orphaned comments section ────────────────────────────
|
|
552
|
+
/**
|
|
553
|
+
* Render orphaned comments (whose line doesn't match any DOM element)
|
|
554
|
+
* in a dedicated section at the bottom of the container.
|
|
555
|
+
*/
|
|
556
|
+
renderOrphanedSection() {
|
|
557
|
+
const lines = this.collectLines();
|
|
558
|
+
const lineSet = new Set(lines);
|
|
559
|
+
const orphanedThreads = [];
|
|
560
|
+
for (const threads of this.threads.values()) {
|
|
561
|
+
for (const thread of threads) {
|
|
562
|
+
if (this.resolveThreadDisplayLine(thread, lineSet) == null) {
|
|
563
|
+
orphanedThreads.push(thread);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (orphanedThreads.length === 0) return;
|
|
568
|
+
const section = document.createElement("div");
|
|
569
|
+
section.className = "mrsf-orphaned-section";
|
|
570
|
+
const heading = document.createElement("div");
|
|
571
|
+
heading.className = "mrsf-orphaned-heading";
|
|
572
|
+
heading.textContent = `Orphaned Comments (${orphanedThreads.length})`;
|
|
573
|
+
section.appendChild(heading);
|
|
574
|
+
const interactive = this.opts.interactive;
|
|
575
|
+
for (const thread of this.orderThreadsForDisplay(orphanedThreads)) {
|
|
576
|
+
const wrapper = document.createElement("div");
|
|
577
|
+
wrapper.className = interactive ? "mrsf-orphaned-thread mrsf-interactive" : "mrsf-orphaned-thread";
|
|
578
|
+
wrapper.innerHTML = renderThreadHtml(thread, interactive);
|
|
579
|
+
section.appendChild(wrapper);
|
|
580
|
+
}
|
|
581
|
+
this.container.appendChild(section);
|
|
582
|
+
this.orphanedSection = section;
|
|
583
|
+
}
|
|
584
|
+
// ── Inline text highlights ──────────────────────────────
|
|
585
|
+
/**
|
|
586
|
+
* For comments with `selected_text`, find the matching text in the DOM
|
|
587
|
+
* and wrap it in a `<mark class="mrsf-inline-highlight">` element with
|
|
588
|
+
* hover/click behaviour to show the comment tooltip.
|
|
589
|
+
*/
|
|
590
|
+
renderInlineHighlights() {
|
|
591
|
+
if (!this.opts.inlineHighlights) return;
|
|
592
|
+
const lineSet = new Set(this.collectLines());
|
|
593
|
+
for (const threads of this.threads.values()) {
|
|
594
|
+
for (const thread of threads) {
|
|
595
|
+
const comment = thread.comment;
|
|
596
|
+
if (!comment.selected_text) continue;
|
|
597
|
+
const displayLine = this.resolveThreadDisplayLine(thread, lineSet);
|
|
598
|
+
if (displayLine == null) continue;
|
|
599
|
+
const el = this.container.querySelector(
|
|
600
|
+
`[data-mrsf-line="${displayLine}"]:not(script):not(.mrsf-gutter):not(.mrsf-gutter-item)`
|
|
601
|
+
);
|
|
602
|
+
if (!el) continue;
|
|
603
|
+
this.wrapSelectedText(el, comment.selected_text, thread, displayLine);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Strip common inline markdown syntax so `selected_text` from source
|
|
609
|
+
* can be matched against rendered text content.
|
|
610
|
+
*/
|
|
611
|
+
static stripInlineMarkdown(text) {
|
|
612
|
+
let s = text;
|
|
613
|
+
s = s.replace(/`([^`]+)`/g, "$1");
|
|
614
|
+
s = s.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
615
|
+
s = s.replace(/__(.+?)__/g, "$1");
|
|
616
|
+
s = s.replace(/\*(.+?)\*/g, "$1");
|
|
617
|
+
s = s.replace(/_(.+?)_/g, "$1");
|
|
618
|
+
s = s.replace(/~~(.+?)~~/g, "$1");
|
|
619
|
+
return s;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Walk text nodes inside `root` to find `text`, then wrap the matching
|
|
623
|
+
* range in a `<mark>` element. Falls back to markdown-stripped matching.
|
|
624
|
+
*/
|
|
625
|
+
wrapSelectedText(root, selectedText, thread, displayLine) {
|
|
626
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
627
|
+
let accumulated = "";
|
|
628
|
+
const textNodes = [];
|
|
629
|
+
let node;
|
|
630
|
+
while (node = walker.nextNode()) {
|
|
631
|
+
const start = accumulated.length;
|
|
632
|
+
accumulated += node.textContent || "";
|
|
633
|
+
textNodes.push({ node, start, end: accumulated.length });
|
|
634
|
+
}
|
|
635
|
+
let matchStart = accumulated.indexOf(selectedText);
|
|
636
|
+
let matchLen = selectedText.length;
|
|
637
|
+
if (matchStart === -1) {
|
|
638
|
+
const stripped = _MrsfController.stripInlineMarkdown(selectedText);
|
|
639
|
+
if (stripped !== selectedText) {
|
|
640
|
+
matchStart = accumulated.indexOf(stripped);
|
|
641
|
+
matchLen = stripped.length;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (matchStart === -1) return;
|
|
645
|
+
const matchEnd = matchStart + matchLen;
|
|
646
|
+
const range = document.createRange();
|
|
647
|
+
let startSet = false;
|
|
648
|
+
for (const tn of textNodes) {
|
|
649
|
+
if (!startSet && tn.end > matchStart) {
|
|
650
|
+
range.setStart(tn.node, matchStart - tn.start);
|
|
651
|
+
startSet = true;
|
|
652
|
+
}
|
|
653
|
+
if (startSet && tn.end >= matchEnd) {
|
|
654
|
+
range.setEnd(tn.node, matchEnd - tn.start);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (!startSet) return;
|
|
659
|
+
const mark = document.createElement("mark");
|
|
660
|
+
mark.className = "mrsf-inline-highlight";
|
|
661
|
+
mark.dataset.mrsfCommentId = thread.comment.id;
|
|
662
|
+
mark.dataset.mrsfLine = String(displayLine);
|
|
663
|
+
try {
|
|
664
|
+
range.surroundContents(mark);
|
|
665
|
+
} catch {
|
|
666
|
+
const fragment = range.extractContents();
|
|
667
|
+
mark.appendChild(fragment);
|
|
668
|
+
range.insertNode(mark);
|
|
669
|
+
}
|
|
670
|
+
this.inlineMarks.push(mark);
|
|
671
|
+
mark.addEventListener("mouseenter", () => {
|
|
672
|
+
this.showInlineTooltip(mark, thread);
|
|
673
|
+
});
|
|
674
|
+
mark.addEventListener("mouseleave", (e) => {
|
|
675
|
+
const related = e.relatedTarget;
|
|
676
|
+
if (related && this.inlineTooltipEl?.contains(related)) return;
|
|
677
|
+
this.scheduleHideInlineTooltip();
|
|
678
|
+
});
|
|
679
|
+
mark.addEventListener("click", (e) => {
|
|
680
|
+
e.stopPropagation();
|
|
681
|
+
if (this.inlineTooltipEl && this.inlineTooltipEl.dataset.mrsfForMark === thread.comment.id) {
|
|
682
|
+
this.hideInlineTooltip();
|
|
683
|
+
} else {
|
|
684
|
+
this.showInlineTooltip(mark, thread);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
showInlineTooltip(mark, thread) {
|
|
689
|
+
this.hideInlineTooltip();
|
|
690
|
+
const tooltip = document.createElement("div");
|
|
691
|
+
tooltip.className = this.opts.interactive ? "mrsf-inline-tooltip mrsf-interactive mrsf-tooltip-visible" : "mrsf-inline-tooltip mrsf-tooltip-visible";
|
|
692
|
+
tooltip.dataset.mrsfForMark = thread.comment.id;
|
|
693
|
+
tooltip.innerHTML = renderThreadHtml(thread, this.opts.interactive);
|
|
694
|
+
this.applyThemeVariables(tooltip);
|
|
695
|
+
tooltip.addEventListener("mouseenter", () => {
|
|
696
|
+
this.cancelHideInlineTooltip();
|
|
697
|
+
});
|
|
698
|
+
tooltip.addEventListener("mouseleave", () => {
|
|
699
|
+
this.hideInlineTooltip();
|
|
700
|
+
});
|
|
701
|
+
document.body.appendChild(tooltip);
|
|
702
|
+
this.inlineTooltipEl = tooltip;
|
|
703
|
+
const rect = mark.getBoundingClientRect();
|
|
704
|
+
const margin = 4;
|
|
705
|
+
const tooltipH = tooltip.offsetHeight;
|
|
706
|
+
if (rect.bottom + margin + tooltipH > window.innerHeight) {
|
|
707
|
+
tooltip.style.top = `${rect.top - tooltipH - margin}px`;
|
|
708
|
+
} else {
|
|
709
|
+
tooltip.style.top = `${rect.bottom + margin}px`;
|
|
710
|
+
}
|
|
711
|
+
tooltip.style.left = `${Math.max(0, rect.left)}px`;
|
|
712
|
+
}
|
|
713
|
+
applyThemeVariables(el) {
|
|
714
|
+
const styles = window.getComputedStyle(this.container);
|
|
715
|
+
const themeVars = [
|
|
716
|
+
"--mrsf-accent",
|
|
717
|
+
"--mrsf-badge-bg",
|
|
718
|
+
"--mrsf-badge-fg",
|
|
719
|
+
"--mrsf-badge-resolved-bg",
|
|
720
|
+
"--mrsf-add-bg",
|
|
721
|
+
"--mrsf-add-fg",
|
|
722
|
+
"--mrsf-add-border",
|
|
723
|
+
"--mrsf-tooltip-bg",
|
|
724
|
+
"--mrsf-tooltip-fg",
|
|
725
|
+
"--mrsf-tooltip-border",
|
|
726
|
+
"--mrsf-highlight-bg",
|
|
727
|
+
"--mrsf-highlight-border",
|
|
728
|
+
"--mrsf-severity-high",
|
|
729
|
+
"--mrsf-severity-medium",
|
|
730
|
+
"--mrsf-severity-low",
|
|
731
|
+
"--mrsf-font-family"
|
|
732
|
+
];
|
|
733
|
+
for (const name of themeVars) {
|
|
734
|
+
const value = styles.getPropertyValue(name).trim();
|
|
735
|
+
if (value) {
|
|
736
|
+
el.style.setProperty(name, value);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
hideInlineTimeout = null;
|
|
741
|
+
scheduleHideInlineTooltip() {
|
|
742
|
+
this.hideInlineTimeout = setTimeout(() => this.hideInlineTooltip(), 120);
|
|
743
|
+
}
|
|
744
|
+
cancelHideInlineTooltip() {
|
|
745
|
+
if (this.hideInlineTimeout) {
|
|
746
|
+
clearTimeout(this.hideInlineTimeout);
|
|
747
|
+
this.hideInlineTimeout = null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
hideInlineTooltip() {
|
|
751
|
+
this.cancelHideInlineTooltip();
|
|
752
|
+
if (this.inlineTooltipEl) {
|
|
753
|
+
this.inlineTooltipEl.remove();
|
|
754
|
+
this.inlineTooltipEl = null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/** Remove all inline marks, unwrapping their contents back to text. */
|
|
758
|
+
removeInlineHighlights() {
|
|
759
|
+
this.hideInlineTooltip();
|
|
760
|
+
for (const mark of this.inlineMarks) {
|
|
761
|
+
const parent = mark.parentNode;
|
|
762
|
+
if (!parent) continue;
|
|
763
|
+
while (mark.firstChild) {
|
|
764
|
+
parent.insertBefore(mark.firstChild, mark);
|
|
765
|
+
}
|
|
766
|
+
parent.removeChild(mark);
|
|
767
|
+
}
|
|
768
|
+
this.inlineMarks = [];
|
|
769
|
+
}
|
|
770
|
+
// ── Tooltip ─────────────────────────────────────────────
|
|
771
|
+
toggleTooltip(anchor, line, threads) {
|
|
772
|
+
if (this.activeTooltip && this.activeTooltip.parentElement === anchor) {
|
|
773
|
+
this.hideTooltip();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
this.hideTooltip();
|
|
777
|
+
this.showTooltip(anchor, line, threads);
|
|
778
|
+
}
|
|
779
|
+
showTooltip(anchor, line, threads) {
|
|
780
|
+
const tooltip = document.createElement("div");
|
|
781
|
+
const interactive = this.opts.interactive;
|
|
782
|
+
tooltip.className = interactive ? "mrsf-tooltip mrsf-interactive mrsf-tooltip-visible" : "mrsf-tooltip mrsf-tooltip-visible";
|
|
783
|
+
tooltip.dataset.mrsfLine = String(line);
|
|
784
|
+
let html = "";
|
|
785
|
+
for (const thread of this.orderThreadsForDisplay(threads)) {
|
|
786
|
+
html += renderThreadHtml(thread, interactive);
|
|
787
|
+
}
|
|
788
|
+
if (interactive) {
|
|
789
|
+
html += `<div class="mrsf-tooltip-actions"><button class="mrsf-action-btn" data-mrsf-action="add" data-mrsf-line="${line}" data-mrsf-start-line="${line}" data-mrsf-end-line="${line}">Add comment</button></div>`;
|
|
790
|
+
}
|
|
791
|
+
tooltip.innerHTML = html;
|
|
792
|
+
anchor.appendChild(tooltip);
|
|
793
|
+
this.activeTooltip = tooltip;
|
|
794
|
+
}
|
|
795
|
+
hideTooltip() {
|
|
796
|
+
if (this.activeTooltip) {
|
|
797
|
+
this.activeTooltip.remove();
|
|
798
|
+
this.activeTooltip = null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// ── Click handling ──────────────────────────────────────
|
|
802
|
+
handleClick(e) {
|
|
803
|
+
const target = e.target.closest("[data-mrsf-action]");
|
|
804
|
+
if (!target && this.activeTooltip) {
|
|
805
|
+
const tooltipClick = e.target.closest(".mrsf-tooltip");
|
|
806
|
+
if (!tooltipClick) {
|
|
807
|
+
this.hideTooltip();
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (!target) return;
|
|
812
|
+
const action = target.dataset.mrsfAction;
|
|
813
|
+
if (!action) return;
|
|
814
|
+
const commentId = target.dataset.mrsfCommentId ?? null;
|
|
815
|
+
const lineStr = target.dataset.mrsfLine;
|
|
816
|
+
const line = lineStr ? parseInt(lineStr, 10) : null;
|
|
817
|
+
const selectionText = target.dataset.mrsfSelection ?? this.lastSelectionText ?? null;
|
|
818
|
+
const startLineStr = target.dataset.mrsfStartLine;
|
|
819
|
+
const endLineStr = target.dataset.mrsfEndLine;
|
|
820
|
+
const startColStr = target.dataset.mrsfStartColumn;
|
|
821
|
+
const endColStr = target.dataset.mrsfEndColumn;
|
|
822
|
+
const startLine = startLineStr ? parseInt(startLineStr, 10) : line ?? null;
|
|
823
|
+
const endLine = endLineStr ? parseInt(endLineStr, 10) : line ?? null;
|
|
824
|
+
const startColumn = startColStr ? parseInt(startColStr, 10) : null;
|
|
825
|
+
const endColumn = endColStr ? parseInt(endColStr, 10) : null;
|
|
826
|
+
e.preventDefault();
|
|
827
|
+
e.stopPropagation();
|
|
828
|
+
const detail = {
|
|
829
|
+
commentId,
|
|
830
|
+
line,
|
|
831
|
+
action,
|
|
832
|
+
selectionText,
|
|
833
|
+
start_line: startLine,
|
|
834
|
+
end_line: endLine,
|
|
835
|
+
start_column: startColumn,
|
|
836
|
+
end_column: endColumn
|
|
837
|
+
};
|
|
838
|
+
if (action === "add" || action === "edit" || action === "reply") {
|
|
839
|
+
if (action === "add") {
|
|
840
|
+
this.hideFloatingAddButton();
|
|
841
|
+
}
|
|
842
|
+
this.openForm(action, detail);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (action === "resolve" || action === "unresolve" || action === "delete") {
|
|
846
|
+
this.openConfirm(action, detail);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
document.dispatchEvent(
|
|
850
|
+
new CustomEvent(`mrsf:${action}`, { detail, bubbles: true })
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
// ── Selection handling ──────────────────────────────────
|
|
854
|
+
handleSelectionChange() {
|
|
855
|
+
const sel = document.getSelection();
|
|
856
|
+
if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
|
|
857
|
+
this.hideFloatingAddButton();
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const text = sel.toString().trim();
|
|
861
|
+
if (!text) {
|
|
862
|
+
this.hideFloatingAddButton();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const range = sel.getRangeAt(0);
|
|
866
|
+
if (!this.selectionBelongsToContainer(range)) {
|
|
867
|
+
this.hideFloatingAddButton();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const rect = range.getBoundingClientRect();
|
|
871
|
+
if (!rect || rect.width === 0 && rect.height === 0) {
|
|
872
|
+
this.hideFloatingAddButton();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const startAnchor = this.findSelectionAnchor(range.startContainer);
|
|
876
|
+
const endAnchor = this.findSelectionAnchor(range.endContainer);
|
|
877
|
+
if (!startAnchor || !endAnchor) {
|
|
878
|
+
this.hideFloatingAddButton();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const startLineStr = startAnchor?.dataset.mrsfStartLine ?? startAnchor?.dataset.mrsfLine;
|
|
882
|
+
const endLineStr = endAnchor?.dataset.mrsfEndLine ?? endAnchor?.dataset.mrsfLine ?? startLineStr;
|
|
883
|
+
const startLine = startLineStr ? parseInt(startLineStr, 10) : null;
|
|
884
|
+
const endLine = endLineStr ? parseInt(endLineStr, 10) : startLine;
|
|
885
|
+
const startColumn = range.startContainer.nodeType === Node.TEXT_NODE ? range.startOffset : null;
|
|
886
|
+
const endColumn = range.endContainer.nodeType === Node.TEXT_NODE ? range.endOffset : null;
|
|
887
|
+
this.showFloatingAddButton(startLine, endLine, startColumn, endColumn, rect, text);
|
|
888
|
+
}
|
|
889
|
+
selectionBelongsToContainer(range) {
|
|
890
|
+
return this.container.contains(range.commonAncestorContainer) && this.container.contains(range.startContainer) && this.container.contains(range.endContainer);
|
|
891
|
+
}
|
|
892
|
+
findSelectionAnchor(node) {
|
|
893
|
+
const element = node instanceof Element ? node : node.parentElement;
|
|
894
|
+
const anchor = element?.closest("[data-mrsf-line]") ?? null;
|
|
895
|
+
return anchor && this.container.contains(anchor) ? anchor : null;
|
|
896
|
+
}
|
|
897
|
+
ensureFloatingAddButton() {
|
|
898
|
+
if (this.floatingAddButton) return this.floatingAddButton;
|
|
899
|
+
const btn = document.createElement("button");
|
|
900
|
+
btn.textContent = "Add comment";
|
|
901
|
+
btn.className = "mrsf-add-inline-button";
|
|
902
|
+
btn.dataset.mrsfAction = "add";
|
|
903
|
+
btn.style.display = "none";
|
|
904
|
+
btn.style.position = "absolute";
|
|
905
|
+
btn.style.zIndex = "1200";
|
|
906
|
+
this.container.appendChild(btn);
|
|
907
|
+
this.floatingAddButton = btn;
|
|
908
|
+
return btn;
|
|
909
|
+
}
|
|
910
|
+
hideFloatingAddButton() {
|
|
911
|
+
if (!this.floatingAddButton) return;
|
|
912
|
+
this.floatingAddButton.style.display = "none";
|
|
913
|
+
this.floatingAddButton.dataset.mrsfLine = "";
|
|
914
|
+
this.floatingAddButton.dataset.mrsfStartLine = "";
|
|
915
|
+
this.floatingAddButton.dataset.mrsfEndLine = "";
|
|
916
|
+
this.lastSelectionText = null;
|
|
917
|
+
}
|
|
918
|
+
showFloatingAddButton(startLine, endLine, startColumn, endColumn, rect, selectionText) {
|
|
919
|
+
const btn = this.ensureFloatingAddButton();
|
|
920
|
+
if (startLine != null) {
|
|
921
|
+
btn.dataset.mrsfLine = String(startLine);
|
|
922
|
+
btn.dataset.mrsfStartLine = String(startLine);
|
|
923
|
+
btn.dataset.mrsfEndLine = String(endLine ?? startLine);
|
|
924
|
+
} else {
|
|
925
|
+
delete btn.dataset.mrsfLine;
|
|
926
|
+
delete btn.dataset.mrsfStartLine;
|
|
927
|
+
delete btn.dataset.mrsfEndLine;
|
|
928
|
+
}
|
|
929
|
+
if (startColumn != null) {
|
|
930
|
+
btn.dataset.mrsfStartColumn = String(startColumn);
|
|
931
|
+
} else {
|
|
932
|
+
delete btn.dataset.mrsfStartColumn;
|
|
933
|
+
}
|
|
934
|
+
if (endColumn != null) {
|
|
935
|
+
btn.dataset.mrsfEndColumn = String(endColumn);
|
|
936
|
+
} else {
|
|
937
|
+
delete btn.dataset.mrsfEndColumn;
|
|
938
|
+
}
|
|
939
|
+
this.lastSelectionText = selectionText;
|
|
940
|
+
const margin = 6;
|
|
941
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
942
|
+
btn.style.visibility = "hidden";
|
|
943
|
+
btn.style.display = "block";
|
|
944
|
+
const width = btn.offsetWidth || 0;
|
|
945
|
+
const height = btn.offsetHeight || 0;
|
|
946
|
+
const minTop = this.container.scrollTop;
|
|
947
|
+
const maxTop = Math.max(minTop, minTop + this.container.clientHeight - height);
|
|
948
|
+
const minLeft = this.container.scrollLeft;
|
|
949
|
+
const maxLeft = Math.max(minLeft, minLeft + this.container.clientWidth - width);
|
|
950
|
+
const preferredTop = rect.top - containerRect.top + this.container.scrollTop - height - margin;
|
|
951
|
+
const fallbackTop = rect.bottom - containerRect.top + this.container.scrollTop + margin;
|
|
952
|
+
const unclampedTop = preferredTop < minTop ? fallbackTop : preferredTop;
|
|
953
|
+
const unclampedLeft = rect.left - containerRect.left + this.container.scrollLeft;
|
|
954
|
+
const top = Math.min(Math.max(unclampedTop, minTop), maxTop);
|
|
955
|
+
const left = Math.min(Math.max(unclampedLeft, minLeft), maxLeft);
|
|
956
|
+
btn.style.top = `${top}px`;
|
|
957
|
+
btn.style.left = `${left}px`;
|
|
958
|
+
btn.style.visibility = "visible";
|
|
959
|
+
}
|
|
960
|
+
// ── Dialog: form (add/edit/reply) ───────────────────────
|
|
961
|
+
injectStyles() {
|
|
962
|
+
if (this.styleInjected) return;
|
|
963
|
+
const css = `
|
|
964
|
+
.mrsf-overlay { position: fixed; inset: 0; background: var(--mrsf-dialog-backdrop, rgba(15, 23, 42, 0.28)); z-index: 2000; display: flex; align-items: center; justify-content: center; padding: 12px; }
|
|
965
|
+
.mrsf-dialog { background: var(--mrsf-dialog-bg, var(--mrsf-tooltip-bg, #0f172a)); color: var(--mrsf-dialog-fg, var(--mrsf-tooltip-fg, #e5eefb)); border: 1px solid var(--mrsf-dialog-border, var(--mrsf-tooltip-border, rgba(148, 163, 184, 0.35))); border-radius: 10px; width: min(420px, calc(100vw - 24px)); box-shadow: 0 18px 48px rgba(15, 23, 42, 0.24); font-family: var(--mrsf-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif); font-size: 13px; overflow: hidden; }
|
|
966
|
+
.mrsf-dialog header { padding: 10px 12px; font-weight: 600; line-height: 1.35; border-bottom: 1px solid var(--mrsf-dialog-border, var(--mrsf-tooltip-border, rgba(148, 163, 184, 0.35))); }
|
|
967
|
+
.mrsf-dialog form { padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
|
968
|
+
.mrsf-dialog-body { padding: 12px; line-height: 1.45; }
|
|
969
|
+
.mrsf-field { display: flex; flex-direction: column; gap: 4px; }
|
|
970
|
+
.mrsf-field label { font-size: 12px; color: var(--mrsf-dialog-muted, color-mix(in srgb, var(--mrsf-dialog-fg, var(--mrsf-tooltip-fg, #e5eefb)) 72%, transparent)); }
|
|
971
|
+
.mrsf-field input, .mrsf-field select, .mrsf-field textarea, .mrsf-field pre { background: var(--mrsf-field-bg, color-mix(in srgb, var(--mrsf-dialog-bg, var(--mrsf-tooltip-bg, #0f172a)) 88%, white)); color: var(--mrsf-dialog-fg, var(--mrsf-tooltip-fg, #e5eefb)); border: 1px solid var(--mrsf-dialog-border, var(--mrsf-tooltip-border, rgba(148, 163, 184, 0.35))); border-radius: 6px; padding: 7px 9px; font-size: 12px; line-height: 1.45; }
|
|
972
|
+
.mrsf-field textarea { min-height: 76px; resize: vertical; }
|
|
973
|
+
.mrsf-field select { min-height: 34px; }
|
|
974
|
+
.mrsf-field pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
975
|
+
.mrsf-field input:focus, .mrsf-field select:focus, .mrsf-field textarea:focus { outline: 2px solid color-mix(in srgb, var(--mrsf-accent, #2563eb) 38%, transparent); outline-offset: 1px; }
|
|
976
|
+
.mrsf-actions-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 0; padding: 10px 12px 12px; border-top: 1px solid var(--mrsf-dialog-border, var(--mrsf-tooltip-border, rgba(148, 163, 184, 0.35))); }
|
|
977
|
+
.mrsf-btn { padding: 5px 10px; border-radius: 999px; border: 1px solid var(--mrsf-dialog-border, var(--mrsf-tooltip-border, rgba(148, 163, 184, 0.35))); background: var(--mrsf-button-bg, color-mix(in srgb, var(--mrsf-dialog-bg, var(--mrsf-tooltip-bg, #0f172a)) 82%, white)); color: var(--mrsf-dialog-fg, var(--mrsf-tooltip-fg, #e5eefb)); cursor: pointer; font: inherit; line-height: 1.2; }
|
|
978
|
+
.mrsf-btn-primary { background: var(--mrsf-button-primary-bg, var(--mrsf-accent, #2563eb)); border-color: var(--mrsf-button-primary-bg, var(--mrsf-accent, #2563eb)); color: #fff; }
|
|
979
|
+
.mrsf-helper { font-size: 11px; color: var(--mrsf-dialog-muted, color-mix(in srgb, var(--mrsf-dialog-fg, var(--mrsf-tooltip-fg, #e5eefb)) 72%, transparent)); }
|
|
980
|
+
`;
|
|
981
|
+
const style = document.createElement("style");
|
|
982
|
+
style.textContent = css;
|
|
983
|
+
document.head.appendChild(style);
|
|
984
|
+
this.styleInjected = true;
|
|
985
|
+
}
|
|
986
|
+
closeOverlay() {
|
|
987
|
+
if (this.overlayEl?.parentElement) {
|
|
988
|
+
this.overlayEl.parentElement.removeChild(this.overlayEl);
|
|
989
|
+
}
|
|
990
|
+
this.overlayEl = null;
|
|
991
|
+
}
|
|
992
|
+
openForm(action, detail) {
|
|
993
|
+
if (window.mrsfDisableBuiltinUi) return;
|
|
994
|
+
this.injectStyles();
|
|
995
|
+
this.closeOverlay();
|
|
996
|
+
const sourceComment = action === "edit" ? this.findCommentById(detail.commentId) : null;
|
|
997
|
+
const selText = detail.selectionText ?? sourceComment?.selected_text ?? "";
|
|
998
|
+
const line = detail.line ?? detail.start_line ?? sourceComment?.line ?? null;
|
|
999
|
+
const endLine = detail.end_line ?? detail.line ?? sourceComment?.end_line ?? sourceComment?.line ?? null;
|
|
1000
|
+
const startCol = detail.start_column ?? sourceComment?.start_column ?? null;
|
|
1001
|
+
const endCol = detail.end_column ?? sourceComment?.end_column ?? null;
|
|
1002
|
+
const overlay = document.createElement("div");
|
|
1003
|
+
overlay.className = "mrsf-overlay";
|
|
1004
|
+
this.applyThemeVariables(overlay);
|
|
1005
|
+
const dialog = document.createElement("div");
|
|
1006
|
+
dialog.className = "mrsf-dialog";
|
|
1007
|
+
const header = document.createElement("header");
|
|
1008
|
+
header.textContent = action === "add" ? "Add comment" : action === "edit" ? "Edit comment" : "Reply";
|
|
1009
|
+
dialog.appendChild(header);
|
|
1010
|
+
const form = document.createElement("form");
|
|
1011
|
+
const field = (labelText, inputEl, helper) => {
|
|
1012
|
+
const wrap = document.createElement("div");
|
|
1013
|
+
wrap.className = "mrsf-field";
|
|
1014
|
+
const label = document.createElement("label");
|
|
1015
|
+
label.textContent = labelText;
|
|
1016
|
+
wrap.appendChild(label);
|
|
1017
|
+
wrap.appendChild(inputEl);
|
|
1018
|
+
if (helper) {
|
|
1019
|
+
const h = document.createElement("div");
|
|
1020
|
+
h.className = "mrsf-helper";
|
|
1021
|
+
h.textContent = helper;
|
|
1022
|
+
wrap.appendChild(h);
|
|
1023
|
+
}
|
|
1024
|
+
form.appendChild(wrap);
|
|
1025
|
+
};
|
|
1026
|
+
const textArea = document.createElement("textarea");
|
|
1027
|
+
textArea.name = "text";
|
|
1028
|
+
textArea.required = true;
|
|
1029
|
+
textArea.value = action === "edit" ? sourceComment?.text ?? "" : "";
|
|
1030
|
+
field("Comment text", textArea);
|
|
1031
|
+
const typeSelect = document.createElement("select");
|
|
1032
|
+
typeSelect.name = "type";
|
|
1033
|
+
["", "suggestion", "issue", "question", "accuracy", "style", "clarity"].forEach((t) => {
|
|
1034
|
+
const opt = document.createElement("option");
|
|
1035
|
+
opt.value = t;
|
|
1036
|
+
opt.textContent = t || "(none)";
|
|
1037
|
+
typeSelect.appendChild(opt);
|
|
1038
|
+
});
|
|
1039
|
+
typeSelect.value = action === "edit" ? sourceComment?.type ?? "" : "";
|
|
1040
|
+
field("Type", typeSelect, "Optional");
|
|
1041
|
+
const severitySelect = document.createElement("select");
|
|
1042
|
+
severitySelect.name = "severity";
|
|
1043
|
+
["", "low", "medium", "high"].forEach((s) => {
|
|
1044
|
+
const opt = document.createElement("option");
|
|
1045
|
+
opt.value = s;
|
|
1046
|
+
opt.textContent = s || "(none)";
|
|
1047
|
+
severitySelect.appendChild(opt);
|
|
1048
|
+
});
|
|
1049
|
+
severitySelect.value = action === "edit" ? sourceComment?.severity ?? "" : "";
|
|
1050
|
+
field("Severity", severitySelect, "Optional");
|
|
1051
|
+
if (selText) {
|
|
1052
|
+
const pre = document.createElement("pre");
|
|
1053
|
+
pre.textContent = selText;
|
|
1054
|
+
field("Selected text", pre, "Captured automatically");
|
|
1055
|
+
}
|
|
1056
|
+
const actions = document.createElement("div");
|
|
1057
|
+
actions.className = "mrsf-actions-row";
|
|
1058
|
+
const cancelBtn = document.createElement("button");
|
|
1059
|
+
cancelBtn.type = "button";
|
|
1060
|
+
cancelBtn.className = "mrsf-btn";
|
|
1061
|
+
cancelBtn.textContent = "Cancel";
|
|
1062
|
+
cancelBtn.addEventListener("click", () => this.closeOverlay());
|
|
1063
|
+
actions.appendChild(cancelBtn);
|
|
1064
|
+
const submitBtn = document.createElement("button");
|
|
1065
|
+
submitBtn.type = "submit";
|
|
1066
|
+
submitBtn.className = "mrsf-btn mrsf-btn-primary";
|
|
1067
|
+
submitBtn.textContent = action === "add" ? "Add" : action === "reply" ? "Reply" : "Save";
|
|
1068
|
+
actions.appendChild(submitBtn);
|
|
1069
|
+
form.appendChild(actions);
|
|
1070
|
+
form.addEventListener("submit", (ev) => {
|
|
1071
|
+
ev.preventDefault();
|
|
1072
|
+
const detailOut = {
|
|
1073
|
+
action,
|
|
1074
|
+
commentId: detail.commentId,
|
|
1075
|
+
text: textArea.value.trim(),
|
|
1076
|
+
type: typeSelect.value || null,
|
|
1077
|
+
severity: severitySelect.value || null,
|
|
1078
|
+
line,
|
|
1079
|
+
end_line: endLine,
|
|
1080
|
+
start_column: startCol,
|
|
1081
|
+
end_column: endCol,
|
|
1082
|
+
selection_text: selText || null
|
|
1083
|
+
};
|
|
1084
|
+
document.dispatchEvent(new CustomEvent("mrsf:submit", { detail: detailOut, bubbles: true }));
|
|
1085
|
+
this.closeOverlay();
|
|
1086
|
+
});
|
|
1087
|
+
dialog.appendChild(form);
|
|
1088
|
+
overlay.appendChild(dialog);
|
|
1089
|
+
overlay.addEventListener("click", (e) => {
|
|
1090
|
+
if (e.target === overlay) this.closeOverlay();
|
|
1091
|
+
});
|
|
1092
|
+
document.body.appendChild(overlay);
|
|
1093
|
+
this.overlayEl = overlay;
|
|
1094
|
+
}
|
|
1095
|
+
// ── Dialog: confirm (resolve / unresolve / delete) ──────
|
|
1096
|
+
openConfirm(action, detail) {
|
|
1097
|
+
if (window.mrsfDisableBuiltinUi) return;
|
|
1098
|
+
this.injectStyles();
|
|
1099
|
+
this.closeOverlay();
|
|
1100
|
+
const overlay = document.createElement("div");
|
|
1101
|
+
overlay.className = "mrsf-overlay";
|
|
1102
|
+
this.applyThemeVariables(overlay);
|
|
1103
|
+
const dialog = document.createElement("div");
|
|
1104
|
+
dialog.className = "mrsf-dialog";
|
|
1105
|
+
const header = document.createElement("header");
|
|
1106
|
+
header.textContent = action === "delete" ? "Delete comment" : "Change status";
|
|
1107
|
+
dialog.appendChild(header);
|
|
1108
|
+
const body = document.createElement("div");
|
|
1109
|
+
body.className = "mrsf-dialog-body";
|
|
1110
|
+
body.textContent = action === "delete" ? "Delete this comment?" : action === "resolve" ? "Mark this comment as resolved?" : "Mark this comment as unresolved?";
|
|
1111
|
+
dialog.appendChild(body);
|
|
1112
|
+
const actions = document.createElement("div");
|
|
1113
|
+
actions.className = "mrsf-actions-row";
|
|
1114
|
+
const cancelBtn = document.createElement("button");
|
|
1115
|
+
cancelBtn.type = "button";
|
|
1116
|
+
cancelBtn.className = "mrsf-btn";
|
|
1117
|
+
cancelBtn.textContent = "Cancel";
|
|
1118
|
+
cancelBtn.addEventListener("click", () => this.closeOverlay());
|
|
1119
|
+
actions.appendChild(cancelBtn);
|
|
1120
|
+
const confirmBtn = document.createElement("button");
|
|
1121
|
+
confirmBtn.type = "button";
|
|
1122
|
+
confirmBtn.className = "mrsf-btn mrsf-btn-primary";
|
|
1123
|
+
confirmBtn.textContent = action === "delete" ? "Delete" : "Confirm";
|
|
1124
|
+
confirmBtn.addEventListener("click", () => {
|
|
1125
|
+
const detailOut = {
|
|
1126
|
+
action,
|
|
1127
|
+
commentId: detail.commentId,
|
|
1128
|
+
text: "",
|
|
1129
|
+
type: null,
|
|
1130
|
+
severity: null,
|
|
1131
|
+
line: detail.line,
|
|
1132
|
+
end_line: detail.end_line ?? detail.line ?? null,
|
|
1133
|
+
start_column: detail.start_column ?? null,
|
|
1134
|
+
end_column: detail.end_column ?? null,
|
|
1135
|
+
selection_text: detail.selectionText ?? null
|
|
1136
|
+
};
|
|
1137
|
+
document.dispatchEvent(new CustomEvent("mrsf:submit", { detail: detailOut, bubbles: true }));
|
|
1138
|
+
this.closeOverlay();
|
|
1139
|
+
});
|
|
1140
|
+
actions.appendChild(confirmBtn);
|
|
1141
|
+
dialog.appendChild(actions);
|
|
1142
|
+
overlay.appendChild(dialog);
|
|
1143
|
+
overlay.addEventListener("click", (e) => {
|
|
1144
|
+
if (e.target === overlay) this.closeOverlay();
|
|
1145
|
+
});
|
|
1146
|
+
document.body.appendChild(overlay);
|
|
1147
|
+
this.overlayEl = overlay;
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
var controllerRegistry = /* @__PURE__ */ new Set();
|
|
1151
|
+
var autoInitDone = false;
|
|
1152
|
+
function refreshAll() {
|
|
1153
|
+
for (const controller of controllerRegistry) {
|
|
1154
|
+
controller.refresh();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
function autoInit() {
|
|
1158
|
+
if (autoInitDone) return;
|
|
1159
|
+
autoInitDone = true;
|
|
1160
|
+
const containers = document.querySelectorAll("[data-mrsf-controller]");
|
|
1161
|
+
for (const container of containers) {
|
|
1162
|
+
const pos = container.dataset.mrsfGutterPosition;
|
|
1163
|
+
const interactive = container.dataset.mrsfInteractive === "true";
|
|
1164
|
+
new MrsfController(container, { gutterPosition: pos ?? "right", interactive });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof document !== "undefined") {
|
|
1168
|
+
if (document.readyState === "loading") {
|
|
1169
|
+
document.addEventListener("DOMContentLoaded", autoInit);
|
|
1170
|
+
} else {
|
|
1171
|
+
autoInit();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
export {
|
|
1175
|
+
MrsfController,
|
|
1176
|
+
autoInit,
|
|
1177
|
+
refreshAll
|
|
1178
|
+
};
|
|
1179
|
+
//# sourceMappingURL=controller.js.map
|