docxodus 10.0.0 → 12.0.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/README.md +7 -5
- package/dist/docxodus.worker.js +6 -38
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor-comments.d.ts +129 -0
- package/dist/editor-comments.d.ts.map +1 -0
- package/dist/editor-comments.js +805 -0
- package/dist/editor-comments.js.map +1 -0
- package/dist/editor-headerfooter.d.ts +125 -85
- package/dist/editor-headerfooter.d.ts.map +1 -1
- package/dist/editor-headerfooter.js +572 -306
- package/dist/editor-headerfooter.js.map +1 -1
- package/dist/editor-image-patch.d.ts +38 -0
- package/dist/editor-image-patch.d.ts.map +1 -0
- package/dist/editor-image-patch.js +101 -0
- package/dist/editor-image-patch.js.map +1 -0
- package/dist/editor.bundle.js +3815 -778
- package/dist/editor.d.ts +237 -5
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1063 -112
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +3911 -849
- package/dist/embed.d.ts +1 -1
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.iife.js +3899 -837
- package/dist/embed.js +10 -1
- package/dist/embed.js.map +1 -1
- package/dist/export-assets.json +40 -46
- package/dist/export-browser.bundle.js +27 -3
- package/dist/index.d.ts +28 -64
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -134
- package/dist/index.js.map +1 -1
- package/dist/pagination.bundle.js +25 -0
- package/dist/pagination.d.ts +6 -0
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +19 -0
- package/dist/pagination.js.map +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js.map +1 -1
- package/dist/ribbon-chrome.d.ts +20 -4
- package/dist/ribbon-chrome.d.ts.map +1 -1
- package/dist/ribbon-chrome.js +575 -184
- package/dist/ribbon-chrome.js.map +1 -1
- package/dist/ribbon.d.ts +5 -4
- package/dist/ribbon.d.ts.map +1 -1
- package/dist/ribbon.js +978 -129
- package/dist/ribbon.js.map +1 -1
- package/dist/session.bundle.js +100 -3
- package/dist/session.d.ts +60 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +75 -3
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +194 -227
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -75
- package/dist/types.js.map +1 -1
- package/dist/viewport.d.ts +7 -0
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +12 -0
- package/dist/viewport.js.map +1 -1
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +31 -37
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.js +39 -3
- package/dist/wasm/_framework/dotnet.native.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/dist/worker-proxy.bundle.js +2 -3
- package/dist/worker-proxy.d.ts +2 -2
- package/dist/worker-proxy.d.ts.map +1 -1
- package/dist/worker-proxy.js +1 -2
- package/dist/worker-proxy.js.map +1 -1
- package/package.json +3 -3
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm.br +0 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommentGutter — Word-style comment bubbles beside the page.
|
|
3
|
+
*
|
|
4
|
+
* The converter renders a commented range inline (`span.comment-highlight[data-comment-id]`
|
|
5
|
+
* around the runs, `a.comment-marker` at the reference), and `DocxSession.ListComments` is the
|
|
6
|
+
* truth about the threads themselves. This module joins the two: for every thread root it finds
|
|
7
|
+
* the highlight in the live DOM, places a bubble in a column beside the sheet at the highlight's
|
|
8
|
+
* vertical position (stacking bubbles downward when they would overlap, the way Word's markup
|
|
9
|
+
* area does), and draws a leader line from the highlight to the bubble.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here is a second source of state. The bubbles are re-derived from `listComments()`
|
|
12
|
+
* plus the DOM on every layout pass, and every action (reply, resolve, edit, delete, post a new
|
|
13
|
+
* comment) goes through the editor's session commands, then re-lays out.
|
|
14
|
+
*
|
|
15
|
+
* The gutter lives INSIDE the editor's scrolling container so bubbles scroll with the page; the
|
|
16
|
+
* host is responsible for reserving horizontal room for it (the ribbon pads the surface).
|
|
17
|
+
*/
|
|
18
|
+
const HIGHLIGHT_SELECTOR = "span.comment-highlight[data-comment-id]";
|
|
19
|
+
const MARKER_SELECTOR = "a.comment-marker[data-comment-id]";
|
|
20
|
+
/** Whether a `data-comment-id` value (possibly "3,5" for overlapping ranges) names `id`. */
|
|
21
|
+
function idsOf(el) {
|
|
22
|
+
return (el.getAttribute("data-comment-id") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
function formatDate(iso) {
|
|
25
|
+
if (!iso)
|
|
26
|
+
return "";
|
|
27
|
+
const d = new Date(iso);
|
|
28
|
+
if (Number.isNaN(d.getTime()))
|
|
29
|
+
return iso;
|
|
30
|
+
try {
|
|
31
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
32
|
+
month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit",
|
|
33
|
+
}).format(d);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return d.toLocaleString();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function initialsOf(author, initials) {
|
|
40
|
+
if (initials && initials.trim())
|
|
41
|
+
return initials.trim().slice(0, 3).toUpperCase();
|
|
42
|
+
const parts = author.trim().split(/\s+/).filter(Boolean);
|
|
43
|
+
if (parts.length === 0)
|
|
44
|
+
return "?";
|
|
45
|
+
return parts.slice(0, 2).map((p) => p[0].toUpperCase()).join("");
|
|
46
|
+
}
|
|
47
|
+
/** A stable per-author hue so every bubble and highlight of one reviewer share a colour. */
|
|
48
|
+
function authorHue(author) {
|
|
49
|
+
let h = 0;
|
|
50
|
+
for (let i = 0; i < author.length; i++)
|
|
51
|
+
h = (h * 31 + author.charCodeAt(i)) >>> 0;
|
|
52
|
+
return h % 360;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every comment hanging off `rootAnchorId`, transitively — replies to replies included — deepest
|
|
56
|
+
* first, so removing them in order never leaves an orphan behind. The root itself is not listed.
|
|
57
|
+
*/
|
|
58
|
+
export function threadMembers(comments, rootAnchorId) {
|
|
59
|
+
const children = new Map();
|
|
60
|
+
for (const c of comments) {
|
|
61
|
+
if (!c.parentAnchorId)
|
|
62
|
+
continue;
|
|
63
|
+
const list = children.get(c.parentAnchorId) ?? [];
|
|
64
|
+
list.push(c.anchorId);
|
|
65
|
+
children.set(c.parentAnchorId, list);
|
|
66
|
+
}
|
|
67
|
+
const out = [];
|
|
68
|
+
const seen = new Set([rootAnchorId]);
|
|
69
|
+
const walk = (id) => {
|
|
70
|
+
for (const child of children.get(id) ?? []) {
|
|
71
|
+
if (seen.has(child))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(child);
|
|
74
|
+
walk(child);
|
|
75
|
+
out.push(child);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
walk(rootAnchorId);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
export class CommentGutter {
|
|
82
|
+
constructor(host, options = {}) {
|
|
83
|
+
this.frame = null;
|
|
84
|
+
this.mutations = null;
|
|
85
|
+
this.resize = null;
|
|
86
|
+
this.activeId = null;
|
|
87
|
+
this.draft = null;
|
|
88
|
+
this.draftTarget = null;
|
|
89
|
+
this.draftAnchorTop = 0;
|
|
90
|
+
this.editing = null;
|
|
91
|
+
this.replying = null;
|
|
92
|
+
this.expanded = new Set();
|
|
93
|
+
this.visible = true;
|
|
94
|
+
this.disposed = false;
|
|
95
|
+
this.host = host;
|
|
96
|
+
this.options = {
|
|
97
|
+
width: options.width ?? 248,
|
|
98
|
+
gap: options.gap ?? 8,
|
|
99
|
+
onChange: options.onChange,
|
|
100
|
+
};
|
|
101
|
+
const doc = host.container.ownerDocument;
|
|
102
|
+
this.element = doc.createElement("aside");
|
|
103
|
+
this.element.className = "docx-comment-gutter";
|
|
104
|
+
this.element.setAttribute("data-docx-comments", "");
|
|
105
|
+
this.element.style.width = `${this.options.width}px`;
|
|
106
|
+
this.leaders = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
107
|
+
this.leaders.setAttribute("class", "docx-comment-leaders");
|
|
108
|
+
this.leaders.setAttribute("aria-hidden", "true");
|
|
109
|
+
if (getComputedStyle(host.container).position === "static") {
|
|
110
|
+
host.container.style.position = "relative";
|
|
111
|
+
}
|
|
112
|
+
host.container.appendChild(this.leaders);
|
|
113
|
+
host.container.appendChild(this.element);
|
|
114
|
+
this.onContainerClick = (event) => {
|
|
115
|
+
const target = event.target;
|
|
116
|
+
if (!target || this.element.contains(target))
|
|
117
|
+
return;
|
|
118
|
+
const hit = target.closest(`${HIGHLIGHT_SELECTOR}, ${MARKER_SELECTOR}`);
|
|
119
|
+
if (!hit)
|
|
120
|
+
return;
|
|
121
|
+
const id = idsOf(hit)[0];
|
|
122
|
+
if (id)
|
|
123
|
+
this.setActive(id, { scrollBubble: true });
|
|
124
|
+
};
|
|
125
|
+
host.container.addEventListener("click", this.onContainerClick);
|
|
126
|
+
if (typeof MutationObserver !== "undefined") {
|
|
127
|
+
this.mutations = new MutationObserver((records) => {
|
|
128
|
+
for (const record of records) {
|
|
129
|
+
const node = record.target;
|
|
130
|
+
if (this.element.contains(node) || this.leaders.contains(node))
|
|
131
|
+
continue;
|
|
132
|
+
this.schedule();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
this.mutations.observe(host.container, {
|
|
137
|
+
childList: true, subtree: true, characterData: true, attributes: true,
|
|
138
|
+
attributeFilter: ["style", "class", "data-anchor"],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
142
|
+
this.resize = new ResizeObserver(() => this.schedule());
|
|
143
|
+
this.resize.observe(host.container);
|
|
144
|
+
}
|
|
145
|
+
this.schedule();
|
|
146
|
+
}
|
|
147
|
+
// ── public surface ──────────────────────────────────────────────────────────
|
|
148
|
+
/** Re-derive and re-position every bubble on the next animation frame. */
|
|
149
|
+
schedule() {
|
|
150
|
+
if (this.disposed || this.frame != null)
|
|
151
|
+
return;
|
|
152
|
+
const view = this.host.container.ownerDocument.defaultView;
|
|
153
|
+
if (!view)
|
|
154
|
+
return;
|
|
155
|
+
this.frame = view.requestAnimationFrame(() => {
|
|
156
|
+
this.frame = null;
|
|
157
|
+
this.layout();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Synchronous layout (tests, and callers that need the DOM settled now). */
|
|
161
|
+
layout() {
|
|
162
|
+
if (this.disposed)
|
|
163
|
+
return;
|
|
164
|
+
const doc = this.host.container.ownerDocument;
|
|
165
|
+
const comments = this.host.listComments();
|
|
166
|
+
const byId = new Map();
|
|
167
|
+
for (const entry of comments)
|
|
168
|
+
byId.set(entry.anchorId, entry);
|
|
169
|
+
const roots = comments.filter((c) => !c.parentAnchorId || !byId.has(c.parentAnchorId));
|
|
170
|
+
const repliesOf = new Map();
|
|
171
|
+
for (const entry of comments) {
|
|
172
|
+
if (!entry.parentAnchorId || !byId.has(entry.parentAnchorId))
|
|
173
|
+
continue;
|
|
174
|
+
let root = byId.get(entry.parentAnchorId);
|
|
175
|
+
// Reply-to-reply chains hang off the thread root, as Word draws them.
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
while (root.parentAnchorId && byId.has(root.parentAnchorId) && !seen.has(root.anchorId)) {
|
|
178
|
+
seen.add(root.anchorId);
|
|
179
|
+
root = byId.get(root.parentAnchorId);
|
|
180
|
+
}
|
|
181
|
+
const list = repliesOf.get(root.anchorId) ?? [];
|
|
182
|
+
list.push(entry);
|
|
183
|
+
repliesOf.set(root.anchorId, list);
|
|
184
|
+
}
|
|
185
|
+
// Reuse bubble nodes across layouts so an open textarea keeps its focus and text.
|
|
186
|
+
const existing = new Map();
|
|
187
|
+
for (const el of Array.from(this.element.querySelectorAll("[data-thread]"))) {
|
|
188
|
+
existing.set(el.dataset.thread, el);
|
|
189
|
+
}
|
|
190
|
+
// Measure with the gutter shown: an empty gutter is display:none, whose rect is all zeros,
|
|
191
|
+
// and every bubble top below is relative to it. The toggle at the end hides it again when
|
|
192
|
+
// nothing is left to show.
|
|
193
|
+
this.element.toggleAttribute("data-empty", roots.length === 0 && !this.draft);
|
|
194
|
+
const containerRect = this.host.container.getBoundingClientRect();
|
|
195
|
+
const gutterRect = this.element.getBoundingClientRect();
|
|
196
|
+
const views = [];
|
|
197
|
+
for (const root of roots) {
|
|
198
|
+
const anchor = this.anchorFor(root);
|
|
199
|
+
const replies = repliesOf.get(root.anchorId) ?? [];
|
|
200
|
+
let bubble = existing.get(root.anchorId);
|
|
201
|
+
existing.delete(root.anchorId);
|
|
202
|
+
const key = this.bubbleKey(root, replies);
|
|
203
|
+
if (!bubble || bubble.dataset.key !== key) {
|
|
204
|
+
const fresh = this.buildBubble(root, replies, doc);
|
|
205
|
+
fresh.dataset.key = key;
|
|
206
|
+
if (bubble)
|
|
207
|
+
bubble.replaceWith(fresh);
|
|
208
|
+
else
|
|
209
|
+
this.element.appendChild(fresh);
|
|
210
|
+
bubble = fresh;
|
|
211
|
+
}
|
|
212
|
+
const rect = anchor?.getBoundingClientRect();
|
|
213
|
+
const desiredTop = rect ? rect.top - gutterRect.top : Number.POSITIVE_INFINITY;
|
|
214
|
+
const desiredLeft = rect ? rect.left : Number.POSITIVE_INFINITY;
|
|
215
|
+
views.push({ root, replies, anchor, bubble, desiredTop, desiredLeft });
|
|
216
|
+
this.markHighlights(root, replies, anchor);
|
|
217
|
+
}
|
|
218
|
+
for (const stale of existing.values())
|
|
219
|
+
stale.remove();
|
|
220
|
+
// Word orders the markup area by document position, not by comment id.
|
|
221
|
+
views.sort((a, b) => a.desiredTop - b.desiredTop ||
|
|
222
|
+
a.desiredLeft - b.desiredLeft ||
|
|
223
|
+
a.root.anchorId.localeCompare(b.root.anchorId));
|
|
224
|
+
const gap = this.options.gap;
|
|
225
|
+
let cursor = 0;
|
|
226
|
+
const placed = [];
|
|
227
|
+
let draftPlaced = false;
|
|
228
|
+
const placeDraft = () => {
|
|
229
|
+
if (!this.draft || draftPlaced)
|
|
230
|
+
return;
|
|
231
|
+
const top = Math.max(this.draftAnchorTop, cursor);
|
|
232
|
+
this.draft.style.top = `${top}px`;
|
|
233
|
+
cursor = top + this.draft.offsetHeight + gap;
|
|
234
|
+
placed.push({ view: null, top, el: this.draft });
|
|
235
|
+
draftPlaced = true;
|
|
236
|
+
};
|
|
237
|
+
for (const view of views) {
|
|
238
|
+
if (this.draft && !draftPlaced && this.draftAnchorTop <= view.desiredTop)
|
|
239
|
+
placeDraft();
|
|
240
|
+
const orphan = !Number.isFinite(view.desiredTop);
|
|
241
|
+
view.bubble.toggleAttribute("data-orphan", orphan);
|
|
242
|
+
const wanted = orphan ? cursor : Math.max(0, view.desiredTop);
|
|
243
|
+
const top = Math.max(wanted, cursor);
|
|
244
|
+
view.bubble.style.top = `${top}px`;
|
|
245
|
+
cursor = top + view.bubble.offsetHeight + gap;
|
|
246
|
+
placed.push({ view, top, el: view.bubble });
|
|
247
|
+
}
|
|
248
|
+
placeDraft();
|
|
249
|
+
// Leader lines: from the highlight's right edge to the bubble's top-left, in the
|
|
250
|
+
// container's coordinate space (the SVG covers the whole scrollable content).
|
|
251
|
+
const scrollW = this.host.container.scrollWidth;
|
|
252
|
+
const scrollH = this.host.container.scrollHeight;
|
|
253
|
+
this.leaders.setAttribute("width", String(scrollW));
|
|
254
|
+
this.leaders.setAttribute("height", String(scrollH));
|
|
255
|
+
this.leaders.style.width = `${scrollW}px`;
|
|
256
|
+
this.leaders.style.height = `${scrollH}px`;
|
|
257
|
+
const paths = [];
|
|
258
|
+
const offX = this.host.container.scrollLeft - containerRect.left;
|
|
259
|
+
const offY = this.host.container.scrollTop - containerRect.top;
|
|
260
|
+
const gutterX = gutterRect.left + offX;
|
|
261
|
+
for (const { view, top } of placed) {
|
|
262
|
+
const rect = view?.anchor?.getBoundingClientRect();
|
|
263
|
+
const active = view ? view.root.anchorId === this.activeId : true;
|
|
264
|
+
const source = rect ?? (view === null && this.draftTarget
|
|
265
|
+
? this.draftTarget.block.getBoundingClientRect()
|
|
266
|
+
: null);
|
|
267
|
+
if (!source)
|
|
268
|
+
continue;
|
|
269
|
+
// Leave the highlight along its baseline, the way Word's connector does: a line at the
|
|
270
|
+
// underline position reads as an extension of the highlight rather than a strike through
|
|
271
|
+
// the rest of the line.
|
|
272
|
+
const x1 = source.right + offX;
|
|
273
|
+
const y1 = source.bottom + offY - 1;
|
|
274
|
+
const y2 = gutterRect.top + offY + top + 14;
|
|
275
|
+
const x2 = gutterX;
|
|
276
|
+
const xm = x2 - 14;
|
|
277
|
+
paths.push(`<path d="M${x1.toFixed(1)} ${y1.toFixed(1)} H${xm.toFixed(1)} L${x2.toFixed(1)} ${y2.toFixed(1)}"` +
|
|
278
|
+
` class="docx-comment-leader${active ? " docx-comment-leader-active" : ""}"/>`);
|
|
279
|
+
}
|
|
280
|
+
this.leaders.innerHTML = paths.join("");
|
|
281
|
+
this.element.toggleAttribute("data-empty", views.length === 0 && !this.draft);
|
|
282
|
+
this.element.hidden = !this.visible;
|
|
283
|
+
this.leaders.style.display = this.visible ? "" : "none";
|
|
284
|
+
this.options.onChange?.({
|
|
285
|
+
threads: views.length,
|
|
286
|
+
open: views.filter((v) => !v.root.resolved).length,
|
|
287
|
+
active: this.activeId,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Re-append the gutter's elements after the container was emptied by a mount. */
|
|
291
|
+
reattach() {
|
|
292
|
+
if (this.disposed)
|
|
293
|
+
return;
|
|
294
|
+
this.host.container.appendChild(this.leaders);
|
|
295
|
+
this.host.container.appendChild(this.element);
|
|
296
|
+
this.schedule();
|
|
297
|
+
}
|
|
298
|
+
/** Show or hide the markup area (Word's "Show Comments"). */
|
|
299
|
+
setVisible(visible) {
|
|
300
|
+
const wasVisible = this.visible;
|
|
301
|
+
this.visible = visible;
|
|
302
|
+
this.element.hidden = !visible;
|
|
303
|
+
this.leaders.style.display = visible ? "" : "none";
|
|
304
|
+
this.host.container.toggleAttribute("data-comments-hidden", !visible);
|
|
305
|
+
// Layouts that ran while hidden measured a zero-height gutter and zero-height bubbles;
|
|
306
|
+
// place everything again now that it can be measured.
|
|
307
|
+
if (visible && !wasVisible)
|
|
308
|
+
this.schedule();
|
|
309
|
+
}
|
|
310
|
+
get isVisible() {
|
|
311
|
+
return this.visible;
|
|
312
|
+
}
|
|
313
|
+
/** The active thread root's anchor id, if any. */
|
|
314
|
+
get active() {
|
|
315
|
+
return this.activeId;
|
|
316
|
+
}
|
|
317
|
+
/** Activate a thread: its bubble lifts, its highlight darkens, and (optionally) it scrolls
|
|
318
|
+
* into view. Pass null to clear. */
|
|
319
|
+
setActive(id, opts = {}) {
|
|
320
|
+
const comments = this.host.listComments();
|
|
321
|
+
// A numeric id (from a highlight) or a cmt anchor id both resolve to the thread root.
|
|
322
|
+
let root;
|
|
323
|
+
if (id != null) {
|
|
324
|
+
const byId = new Map(comments.map((c) => [c.anchorId, c]));
|
|
325
|
+
root = comments.find((c) => c.anchorId === id || String(c.id) === id);
|
|
326
|
+
const seen = new Set();
|
|
327
|
+
while (root?.parentAnchorId && byId.has(root.parentAnchorId) && !seen.has(root.anchorId)) {
|
|
328
|
+
seen.add(root.anchorId);
|
|
329
|
+
root = byId.get(root.parentAnchorId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
this.activeId = root?.anchorId ?? null;
|
|
333
|
+
for (const el of Array.from(this.element.querySelectorAll("[data-thread]"))) {
|
|
334
|
+
el.toggleAttribute("data-active", el.dataset.thread === this.activeId);
|
|
335
|
+
}
|
|
336
|
+
for (const el of Array.from(this.host.container.querySelectorAll(HIGHLIGHT_SELECTOR))) {
|
|
337
|
+
el.classList.toggle("docx-comment-active", !!root && idsOf(el).includes(String(root.id)));
|
|
338
|
+
}
|
|
339
|
+
if (root && opts.scrollBubble) {
|
|
340
|
+
this.element.querySelector(`[data-thread="${CSS.escape(root.anchorId)}"]`)
|
|
341
|
+
?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
342
|
+
}
|
|
343
|
+
if (root && opts.scrollAnchor) {
|
|
344
|
+
this.anchorFor(root)?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
345
|
+
}
|
|
346
|
+
this.schedule();
|
|
347
|
+
}
|
|
348
|
+
/** Thread roots in document order — what Previous/Next step through. */
|
|
349
|
+
threadsInOrder() {
|
|
350
|
+
const comments = this.host.listComments();
|
|
351
|
+
const byId = new Set(comments.map((c) => c.anchorId));
|
|
352
|
+
const roots = comments.filter((c) => !c.parentAnchorId || !byId.has(c.parentAnchorId));
|
|
353
|
+
return roots
|
|
354
|
+
.map((root) => {
|
|
355
|
+
const rect = this.anchorFor(root)?.getBoundingClientRect();
|
|
356
|
+
return { root, top: rect?.top ?? Number.POSITIVE_INFINITY, left: rect?.left ?? Number.POSITIVE_INFINITY };
|
|
357
|
+
})
|
|
358
|
+
.sort((a, b) => a.top - b.top || a.left - b.left || a.root.anchorId.localeCompare(b.root.anchorId))
|
|
359
|
+
.map((x) => x.root);
|
|
360
|
+
}
|
|
361
|
+
/** Activate the thread after (or before) the active one, wrapping around. */
|
|
362
|
+
step(direction) {
|
|
363
|
+
const order = this.threadsInOrder();
|
|
364
|
+
if (order.length === 0)
|
|
365
|
+
return null;
|
|
366
|
+
const index = order.findIndex((c) => c.anchorId === this.activeId);
|
|
367
|
+
const next = index < 0
|
|
368
|
+
? (direction > 0 ? order[0] : order[order.length - 1])
|
|
369
|
+
: order[(index + direction + order.length) % order.length];
|
|
370
|
+
this.setActive(next.anchorId, { scrollBubble: true, scrollAnchor: true });
|
|
371
|
+
return next;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Start a new comment on the current selection: a draft bubble appears beside it with a
|
|
375
|
+
* focused textarea. Posting goes through the editor; cancelling removes the draft. Returns
|
|
376
|
+
* false when nothing is selected/focused to comment on.
|
|
377
|
+
*/
|
|
378
|
+
beginDraft() {
|
|
379
|
+
const target = this.host.commentTarget();
|
|
380
|
+
if (!target)
|
|
381
|
+
return false;
|
|
382
|
+
this.cancelDraft();
|
|
383
|
+
this.draftTarget = target;
|
|
384
|
+
const doc = this.host.container.ownerDocument;
|
|
385
|
+
// The first comment in a document drafts against a gutter that is still display:none
|
|
386
|
+
// (no threads); its rect is zeros until shown, and the draft's top is relative to it.
|
|
387
|
+
this.element.removeAttribute("data-empty");
|
|
388
|
+
const gutterRect = this.element.getBoundingClientRect();
|
|
389
|
+
const sel = doc.defaultView?.getSelection();
|
|
390
|
+
let rect = null;
|
|
391
|
+
if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
|
|
392
|
+
const r = sel.getRangeAt(0).getBoundingClientRect();
|
|
393
|
+
if (r.height > 0)
|
|
394
|
+
rect = r;
|
|
395
|
+
}
|
|
396
|
+
rect ?? (rect = target.block.getBoundingClientRect());
|
|
397
|
+
this.draftAnchorTop = Math.max(0, rect.top - gutterRect.top);
|
|
398
|
+
const bubble = doc.createElement("div");
|
|
399
|
+
bubble.className = "docx-comment-bubble";
|
|
400
|
+
bubble.setAttribute("data-draft", "");
|
|
401
|
+
bubble.setAttribute("data-active", "");
|
|
402
|
+
bubble.style.setProperty("--docx-comment-hue", String(authorHue(this.host.commentAuthor)));
|
|
403
|
+
bubble.innerHTML =
|
|
404
|
+
`<div class="docx-comment-head"><span class="docx-comment-avatar">${initialsOf(this.host.commentAuthor)}</span>` +
|
|
405
|
+
`<span class="docx-comment-author"></span></div>` +
|
|
406
|
+
`<textarea class="docx-comment-input" rows="3" placeholder="Type a comment…" data-comment-draft-text></textarea>` +
|
|
407
|
+
`<div class="docx-comment-actions">` +
|
|
408
|
+
`<button type="button" class="docx-comment-primary" data-comment-action="post">Post</button>` +
|
|
409
|
+
`<button type="button" data-comment-action="cancel">Cancel</button></div>`;
|
|
410
|
+
bubble.querySelector(".docx-comment-author").textContent = this.host.commentAuthor;
|
|
411
|
+
const input = bubble.querySelector("textarea");
|
|
412
|
+
const post = () => {
|
|
413
|
+
const text = input.value.trim();
|
|
414
|
+
if (!text) {
|
|
415
|
+
input.focus();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const created = this.host.addComment(text, this.host.commentAuthor, target);
|
|
419
|
+
this.cancelDraft();
|
|
420
|
+
if (created)
|
|
421
|
+
this.setActive(created.anchorId, { scrollBubble: true });
|
|
422
|
+
else
|
|
423
|
+
this.schedule();
|
|
424
|
+
};
|
|
425
|
+
bubble.querySelector('[data-comment-action="post"]').addEventListener("click", post);
|
|
426
|
+
bubble.querySelector('[data-comment-action="cancel"]').addEventListener("click", () => this.cancelDraft());
|
|
427
|
+
input.addEventListener("keydown", (event) => {
|
|
428
|
+
const key = event;
|
|
429
|
+
if (key.key === "Escape") {
|
|
430
|
+
key.preventDefault();
|
|
431
|
+
this.cancelDraft();
|
|
432
|
+
}
|
|
433
|
+
if (key.key === "Enter" && (key.ctrlKey || key.metaKey)) {
|
|
434
|
+
key.preventDefault();
|
|
435
|
+
post();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
// Mousedown on the draft must not collapse the document selection until the target is
|
|
439
|
+
// captured — it already is, so let the textarea take focus normally.
|
|
440
|
+
this.draft = bubble;
|
|
441
|
+
this.element.appendChild(bubble);
|
|
442
|
+
this.activeId = null;
|
|
443
|
+
this.layout();
|
|
444
|
+
input.focus();
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
/** Remove the draft bubble, if any. */
|
|
448
|
+
cancelDraft() {
|
|
449
|
+
if (!this.draft)
|
|
450
|
+
return;
|
|
451
|
+
this.draft.remove();
|
|
452
|
+
this.draft = null;
|
|
453
|
+
this.draftTarget = null;
|
|
454
|
+
this.schedule();
|
|
455
|
+
}
|
|
456
|
+
get hasDraft() {
|
|
457
|
+
return this.draft !== null;
|
|
458
|
+
}
|
|
459
|
+
dispose() {
|
|
460
|
+
if (this.disposed)
|
|
461
|
+
return;
|
|
462
|
+
this.disposed = true;
|
|
463
|
+
const view = this.host.container.ownerDocument.defaultView;
|
|
464
|
+
if (this.frame != null)
|
|
465
|
+
view?.cancelAnimationFrame(this.frame);
|
|
466
|
+
this.frame = null;
|
|
467
|
+
this.mutations?.disconnect();
|
|
468
|
+
this.resize?.disconnect();
|
|
469
|
+
this.host.container.removeEventListener("click", this.onContainerClick);
|
|
470
|
+
this.element.remove();
|
|
471
|
+
this.leaders.remove();
|
|
472
|
+
}
|
|
473
|
+
// ── internals ───────────────────────────────────────────────────────────────
|
|
474
|
+
/** The first highlight (else marker) carrying the thread's numeric id, in the live DOM. */
|
|
475
|
+
anchorFor(root) {
|
|
476
|
+
const id = String(root.id);
|
|
477
|
+
const candidates = this.host.container.querySelectorAll(`${HIGHLIGHT_SELECTOR}, ${MARKER_SELECTOR}`);
|
|
478
|
+
let marker = null;
|
|
479
|
+
for (const el of Array.from(candidates)) {
|
|
480
|
+
if (this.element.contains(el))
|
|
481
|
+
continue;
|
|
482
|
+
if (!idsOf(el).includes(id))
|
|
483
|
+
continue;
|
|
484
|
+
// Hidden staging/registry copies must never win over the visible page.
|
|
485
|
+
if (el.closest("#pagination-staging, [id^='pagination-'][style*='display:none'], [data-hf-inert]"))
|
|
486
|
+
continue;
|
|
487
|
+
if (el.matches(HIGHLIGHT_SELECTOR))
|
|
488
|
+
return el;
|
|
489
|
+
marker ?? (marker = el);
|
|
490
|
+
}
|
|
491
|
+
return marker;
|
|
492
|
+
}
|
|
493
|
+
markHighlights(root, replies, anchor) {
|
|
494
|
+
const ids = new Set([String(root.id), ...replies.map((r) => String(r.id))]);
|
|
495
|
+
const hue = String(authorHue(root.author || ""));
|
|
496
|
+
for (const el of Array.from(this.host.container.querySelectorAll(HIGHLIGHT_SELECTOR))) {
|
|
497
|
+
if (this.element.contains(el))
|
|
498
|
+
continue;
|
|
499
|
+
if (!idsOf(el).some((x) => ids.has(x)))
|
|
500
|
+
continue;
|
|
501
|
+
// Only write what changed: the document's MutationObserver schedules a layout on every
|
|
502
|
+
// style/class mutation, so an unconditional write here would lay out forever.
|
|
503
|
+
if (el.style.getPropertyValue("--docx-comment-hue") !== hue)
|
|
504
|
+
el.style.setProperty("--docx-comment-hue", hue);
|
|
505
|
+
el.toggleAttribute("data-comment-resolved", !!root.resolved);
|
|
506
|
+
el.classList.toggle("docx-comment-active", root.anchorId === this.activeId);
|
|
507
|
+
}
|
|
508
|
+
void anchor;
|
|
509
|
+
}
|
|
510
|
+
bubbleKey(root, replies) {
|
|
511
|
+
const parts = [root.anchorId, root.author, root.date ?? "", root.text, root.resolved ? "r" : "o"];
|
|
512
|
+
for (const r of replies)
|
|
513
|
+
parts.push(r.anchorId, r.author, r.date ?? "", r.text);
|
|
514
|
+
parts.push(this.editing === root.anchorId ? "edit" : "", this.replying === root.anchorId ? "reply" : "");
|
|
515
|
+
parts.push(this.expanded.has(root.anchorId) ? "x" : "");
|
|
516
|
+
for (const r of replies)
|
|
517
|
+
if (this.editing === r.anchorId)
|
|
518
|
+
parts.push(`edit:${r.anchorId}`);
|
|
519
|
+
return parts.join("");
|
|
520
|
+
}
|
|
521
|
+
buildBubble(root, replies, doc) {
|
|
522
|
+
const bubble = doc.createElement("div");
|
|
523
|
+
bubble.className = "docx-comment-bubble";
|
|
524
|
+
bubble.dataset.thread = root.anchorId;
|
|
525
|
+
bubble.dataset.commentId = String(root.id);
|
|
526
|
+
bubble.style.setProperty("--docx-comment-hue", String(authorHue(root.author || "")));
|
|
527
|
+
if (root.resolved)
|
|
528
|
+
bubble.setAttribute("data-resolved", "");
|
|
529
|
+
if (root.anchorId === this.activeId)
|
|
530
|
+
bubble.setAttribute("data-active", "");
|
|
531
|
+
const collapsed = !!root.resolved && !this.expanded.has(root.anchorId);
|
|
532
|
+
if (collapsed)
|
|
533
|
+
bubble.setAttribute("data-collapsed", "");
|
|
534
|
+
bubble.appendChild(this.buildEntry(root, doc, true));
|
|
535
|
+
for (const reply of replies)
|
|
536
|
+
bubble.appendChild(this.buildEntry(reply, doc, false));
|
|
537
|
+
if (!collapsed) {
|
|
538
|
+
if (this.replying === root.anchorId) {
|
|
539
|
+
const input = doc.createElement("textarea");
|
|
540
|
+
input.className = "docx-comment-input";
|
|
541
|
+
input.rows = 2;
|
|
542
|
+
input.placeholder = "Reply…";
|
|
543
|
+
input.setAttribute("data-comment-reply-text", "");
|
|
544
|
+
const actions = doc.createElement("div");
|
|
545
|
+
actions.className = "docx-comment-actions";
|
|
546
|
+
const post = doc.createElement("button");
|
|
547
|
+
post.type = "button";
|
|
548
|
+
post.className = "docx-comment-primary";
|
|
549
|
+
post.textContent = "Reply";
|
|
550
|
+
post.setAttribute("data-comment-action", "post-reply");
|
|
551
|
+
const cancel = doc.createElement("button");
|
|
552
|
+
cancel.type = "button";
|
|
553
|
+
cancel.textContent = "Cancel";
|
|
554
|
+
cancel.setAttribute("data-comment-action", "cancel-reply");
|
|
555
|
+
const submit = () => {
|
|
556
|
+
const text = input.value.trim();
|
|
557
|
+
if (!text) {
|
|
558
|
+
input.focus();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.replying = null;
|
|
562
|
+
this.host.addCommentReply(root.anchorId, text, this.host.commentAuthor);
|
|
563
|
+
this.setActive(root.anchorId);
|
|
564
|
+
};
|
|
565
|
+
post.addEventListener("click", submit);
|
|
566
|
+
cancel.addEventListener("click", () => { this.replying = null; this.schedule(); });
|
|
567
|
+
input.addEventListener("keydown", (event) => {
|
|
568
|
+
const key = event;
|
|
569
|
+
if (key.key === "Escape") {
|
|
570
|
+
key.preventDefault();
|
|
571
|
+
this.replying = null;
|
|
572
|
+
this.schedule();
|
|
573
|
+
}
|
|
574
|
+
if (key.key === "Enter" && (key.ctrlKey || key.metaKey)) {
|
|
575
|
+
key.preventDefault();
|
|
576
|
+
submit();
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
actions.append(post, cancel);
|
|
580
|
+
bubble.append(input, actions);
|
|
581
|
+
queueMicrotask(() => input.focus());
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
const bar = doc.createElement("div");
|
|
585
|
+
bar.className = "docx-comment-bar";
|
|
586
|
+
const reply = doc.createElement("button");
|
|
587
|
+
reply.type = "button";
|
|
588
|
+
reply.textContent = "Reply";
|
|
589
|
+
reply.setAttribute("data-comment-action", "reply");
|
|
590
|
+
reply.addEventListener("click", () => {
|
|
591
|
+
this.replying = root.anchorId;
|
|
592
|
+
this.editing = null;
|
|
593
|
+
this.setActive(root.anchorId);
|
|
594
|
+
// Build the reply box now, not on the next frame: the click handler returns before a
|
|
595
|
+
// scheduled layout would run, and a keystroke that lands in between goes to the page
|
|
596
|
+
// instead of the textarea — the first letter of a quickly typed reply vanished.
|
|
597
|
+
this.layout();
|
|
598
|
+
});
|
|
599
|
+
const resolve = doc.createElement("button");
|
|
600
|
+
resolve.type = "button";
|
|
601
|
+
resolve.textContent = root.resolved ? "Reopen" : "Resolve";
|
|
602
|
+
resolve.setAttribute("data-comment-action", root.resolved ? "reopen" : "resolve");
|
|
603
|
+
resolve.addEventListener("click", () => {
|
|
604
|
+
this.host.setCommentResolved(root.anchorId, !root.resolved);
|
|
605
|
+
if (!root.resolved)
|
|
606
|
+
this.expanded.delete(root.anchorId);
|
|
607
|
+
this.setActive(root.anchorId);
|
|
608
|
+
});
|
|
609
|
+
bar.append(reply, resolve);
|
|
610
|
+
bubble.appendChild(bar);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
bubble.addEventListener("mousedown", (event) => {
|
|
614
|
+
// Buttons and inputs must keep working; a click on the bubble body just activates it.
|
|
615
|
+
const t = event.target;
|
|
616
|
+
if (t.closest("button, textarea, input, a"))
|
|
617
|
+
return;
|
|
618
|
+
if (collapsed) {
|
|
619
|
+
this.expanded.add(root.anchorId);
|
|
620
|
+
}
|
|
621
|
+
this.setActive(root.anchorId, { scrollAnchor: true });
|
|
622
|
+
});
|
|
623
|
+
return bubble;
|
|
624
|
+
}
|
|
625
|
+
buildEntry(entry, doc, isRoot) {
|
|
626
|
+
const el = doc.createElement("div");
|
|
627
|
+
el.className = isRoot ? "docx-comment-entry docx-comment-root" : "docx-comment-entry docx-comment-reply";
|
|
628
|
+
el.dataset.comment = entry.anchorId;
|
|
629
|
+
const head = doc.createElement("div");
|
|
630
|
+
head.className = "docx-comment-head";
|
|
631
|
+
const avatar = doc.createElement("span");
|
|
632
|
+
avatar.className = "docx-comment-avatar";
|
|
633
|
+
avatar.textContent = initialsOf(entry.author || "", entry.initials);
|
|
634
|
+
avatar.style.setProperty("--docx-comment-hue", String(authorHue(entry.author || "")));
|
|
635
|
+
const author = doc.createElement("span");
|
|
636
|
+
author.className = "docx-comment-author";
|
|
637
|
+
author.textContent = entry.author || "Unknown";
|
|
638
|
+
const date = doc.createElement("span");
|
|
639
|
+
date.className = "docx-comment-date";
|
|
640
|
+
date.textContent = formatDate(entry.date);
|
|
641
|
+
head.append(avatar, author, date);
|
|
642
|
+
if (isRoot && entry.resolved) {
|
|
643
|
+
const badge = doc.createElement("span");
|
|
644
|
+
badge.className = "docx-comment-badge";
|
|
645
|
+
badge.textContent = "Resolved";
|
|
646
|
+
head.appendChild(badge);
|
|
647
|
+
}
|
|
648
|
+
const menu = doc.createElement("span");
|
|
649
|
+
menu.className = "docx-comment-menu";
|
|
650
|
+
const edit = doc.createElement("button");
|
|
651
|
+
edit.type = "button";
|
|
652
|
+
edit.title = "Edit";
|
|
653
|
+
edit.setAttribute("aria-label", "Edit comment");
|
|
654
|
+
edit.setAttribute("data-comment-action", "edit");
|
|
655
|
+
edit.innerHTML = "✎";
|
|
656
|
+
edit.addEventListener("click", () => { this.editing = entry.anchorId; this.replying = null; this.layout(); });
|
|
657
|
+
const del = doc.createElement("button");
|
|
658
|
+
del.type = "button";
|
|
659
|
+
del.title = isRoot ? "Delete thread" : "Delete reply";
|
|
660
|
+
del.setAttribute("aria-label", del.title);
|
|
661
|
+
del.setAttribute("data-comment-action", "delete");
|
|
662
|
+
del.innerHTML = "🗑";
|
|
663
|
+
del.addEventListener("click", () => {
|
|
664
|
+
if (isRoot) {
|
|
665
|
+
// A deleted root would orphan its replies into top-level comments; take the thread
|
|
666
|
+
// down, replies to replies included.
|
|
667
|
+
for (const id of threadMembers(this.host.listComments(), entry.anchorId))
|
|
668
|
+
this.host.removeComment(id);
|
|
669
|
+
}
|
|
670
|
+
this.host.removeComment(entry.anchorId);
|
|
671
|
+
if (this.activeId === entry.anchorId)
|
|
672
|
+
this.activeId = null;
|
|
673
|
+
this.schedule();
|
|
674
|
+
});
|
|
675
|
+
menu.append(edit, del);
|
|
676
|
+
head.appendChild(menu);
|
|
677
|
+
el.appendChild(head);
|
|
678
|
+
if (this.editing === entry.anchorId) {
|
|
679
|
+
const input = doc.createElement("textarea");
|
|
680
|
+
input.className = "docx-comment-input";
|
|
681
|
+
input.rows = 3;
|
|
682
|
+
input.value = entry.text;
|
|
683
|
+
input.setAttribute("data-comment-edit-text", "");
|
|
684
|
+
const actions = doc.createElement("div");
|
|
685
|
+
actions.className = "docx-comment-actions";
|
|
686
|
+
const save = doc.createElement("button");
|
|
687
|
+
save.type = "button";
|
|
688
|
+
save.className = "docx-comment-primary";
|
|
689
|
+
save.textContent = "Save";
|
|
690
|
+
save.setAttribute("data-comment-action", "save");
|
|
691
|
+
const cancel = doc.createElement("button");
|
|
692
|
+
cancel.type = "button";
|
|
693
|
+
cancel.textContent = "Cancel";
|
|
694
|
+
cancel.setAttribute("data-comment-action", "cancel-edit");
|
|
695
|
+
const submit = () => {
|
|
696
|
+
const text = input.value.trim();
|
|
697
|
+
this.editing = null;
|
|
698
|
+
if (text && text !== entry.text)
|
|
699
|
+
this.host.updateComment(entry.anchorId, text);
|
|
700
|
+
this.schedule();
|
|
701
|
+
};
|
|
702
|
+
save.addEventListener("click", submit);
|
|
703
|
+
cancel.addEventListener("click", () => { this.editing = null; this.schedule(); });
|
|
704
|
+
input.addEventListener("keydown", (event) => {
|
|
705
|
+
const key = event;
|
|
706
|
+
if (key.key === "Escape") {
|
|
707
|
+
key.preventDefault();
|
|
708
|
+
this.editing = null;
|
|
709
|
+
this.schedule();
|
|
710
|
+
}
|
|
711
|
+
if (key.key === "Enter" && (key.ctrlKey || key.metaKey)) {
|
|
712
|
+
key.preventDefault();
|
|
713
|
+
submit();
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
actions.append(save, cancel);
|
|
717
|
+
el.append(input, actions);
|
|
718
|
+
queueMicrotask(() => { input.focus(); input.setSelectionRange(input.value.length, input.value.length); });
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
const body = doc.createElement("div");
|
|
722
|
+
body.className = "docx-comment-text";
|
|
723
|
+
body.textContent = entry.text;
|
|
724
|
+
el.appendChild(body);
|
|
725
|
+
}
|
|
726
|
+
return el;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/** The gutter's stylesheet — injected once per document by the editor. Scoped to the classes
|
|
730
|
+
* this module emits, so it is safe on a host page. Colours derive from a per-author hue so a
|
|
731
|
+
* reviewer's highlight and bubble read as one. */
|
|
732
|
+
export const COMMENT_GUTTER_CSS = `
|
|
733
|
+
.docx-comment-gutter {
|
|
734
|
+
position: absolute; top: 0; right: 0; bottom: 0; z-index: 4;
|
|
735
|
+
pointer-events: none; font: 12.5px/1.4 "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
736
|
+
}
|
|
737
|
+
.docx-comment-gutter[data-empty] { display: none; }
|
|
738
|
+
.docx-comment-leaders { position: absolute; top: 0; left: 0; z-index: 3; pointer-events: none; overflow: visible; }
|
|
739
|
+
.docx-comment-leader { fill: none; stroke: hsl(var(--docx-comment-hue, 200) 45% 62%); stroke-width: 1; stroke-dasharray: 3 3; opacity: .55; }
|
|
740
|
+
.docx-comment-leader-active { stroke-dasharray: none; opacity: .95; stroke-width: 1.5; }
|
|
741
|
+
.docx-comment-bubble {
|
|
742
|
+
position: absolute; left: 12px; right: 8px; pointer-events: auto;
|
|
743
|
+
padding: 9px 10px 8px; border: 1px solid hsl(var(--docx-comment-hue, 200) 40% 78%);
|
|
744
|
+
border-left: 3px solid hsl(var(--docx-comment-hue, 200) 55% 55%);
|
|
745
|
+
border-radius: 8px; background: #fff; color: #1e293b;
|
|
746
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, .06), 0 3px 8px rgba(15, 23, 42, .05);
|
|
747
|
+
transition: top .18s cubic-bezier(.4,0,.2,1), box-shadow .15s ease, transform .15s ease;
|
|
748
|
+
}
|
|
749
|
+
.docx-comment-bubble[data-active] {
|
|
750
|
+
border-color: hsl(var(--docx-comment-hue, 200) 55% 55%);
|
|
751
|
+
box-shadow: 0 2px 6px rgba(15, 23, 42, .08), 0 10px 22px rgba(15, 23, 42, .1);
|
|
752
|
+
transform: translateX(-4px); z-index: 2;
|
|
753
|
+
}
|
|
754
|
+
.docx-comment-bubble[data-resolved] { opacity: .72; background: #f8fafc; }
|
|
755
|
+
.docx-comment-bubble[data-collapsed] .docx-comment-text { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; color: #64748b; }
|
|
756
|
+
.docx-comment-bubble[data-collapsed] .docx-comment-reply, .docx-comment-bubble[data-collapsed] .docx-comment-bar { display: none; }
|
|
757
|
+
.docx-comment-bubble[data-orphan] { border-style: dashed; }
|
|
758
|
+
.docx-comment-bubble[data-draft] { border-style: solid; }
|
|
759
|
+
.docx-comment-entry + .docx-comment-entry { margin-top: 8px; padding-top: 8px; border-top: 1px solid #f1f5f9; }
|
|
760
|
+
.docx-comment-reply { margin-left: 10px; }
|
|
761
|
+
.docx-comment-head { display: flex; align-items: center; gap: 6px; min-height: 20px; }
|
|
762
|
+
.docx-comment-avatar {
|
|
763
|
+
display: inline-grid; place-items: center; flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;
|
|
764
|
+
background: hsl(var(--docx-comment-hue, 200) 55% 45%); color: #fff; font-size: 9px; font-weight: 700; letter-spacing: .02em;
|
|
765
|
+
}
|
|
766
|
+
.docx-comment-author { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
767
|
+
.docx-comment-date { color: #94a3b8; font-size: 10.5px; white-space: nowrap; margin-left: auto; }
|
|
768
|
+
.docx-comment-badge { padding: 1px 6px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
|
|
769
|
+
.docx-comment-menu { display: none; gap: 2px; margin-left: 4px; }
|
|
770
|
+
.docx-comment-bubble:hover .docx-comment-menu, .docx-comment-bubble[data-active] .docx-comment-menu { display: inline-flex; }
|
|
771
|
+
.docx-comment-menu button {
|
|
772
|
+
padding: 0 4px; min-height: 18px; border: 0; border-radius: 4px; background: none; color: #94a3b8; font-size: 12px; line-height: 1; cursor: pointer;
|
|
773
|
+
}
|
|
774
|
+
.docx-comment-menu button:hover { background: #f1f5f9; color: #1e293b; }
|
|
775
|
+
.docx-comment-text { margin: 5px 0 0; white-space: pre-wrap; word-wrap: break-word; color: #334155; }
|
|
776
|
+
.docx-comment-bar { display: flex; gap: 6px; margin-top: 8px; }
|
|
777
|
+
.docx-comment-bar button, .docx-comment-actions button {
|
|
778
|
+
min-height: 24px; padding: 2px 9px; border: 1px solid #e2e8f0; border-radius: 6px; background: #fff; color: #1e293b;
|
|
779
|
+
font: inherit; font-size: 11.5px; font-weight: 500; cursor: pointer;
|
|
780
|
+
}
|
|
781
|
+
.docx-comment-bar button:hover, .docx-comment-actions button:hover { background: #f1f5f9; }
|
|
782
|
+
.docx-comment-actions { display: flex; gap: 6px; margin-top: 6px; }
|
|
783
|
+
.docx-comment-actions .docx-comment-primary { background: #0f766e; border-color: #0f766e; color: #fff; }
|
|
784
|
+
.docx-comment-actions .docx-comment-primary:hover { background: #0d9488; }
|
|
785
|
+
.docx-comment-input {
|
|
786
|
+
display: block; width: 100%; margin-top: 6px; padding: 6px 8px; border: 1px solid #cbd5e1; border-radius: 6px;
|
|
787
|
+
background: #fff; color: #1e293b; font: inherit; font-size: 12.5px; line-height: 1.4; resize: vertical; box-sizing: border-box;
|
|
788
|
+
}
|
|
789
|
+
.docx-comment-input:focus { outline: 2px solid #0f766e; outline-offset: 1px; border-color: #0f766e; }
|
|
790
|
+
/* The commented range itself. The converter's stylesheet paints a flat yellow; the editor
|
|
791
|
+
tints by author and lifts the active thread the way Word does. */
|
|
792
|
+
span.comment-highlight[data-comment-id] {
|
|
793
|
+
background: hsl(var(--docx-comment-hue, 45) 90% 88%);
|
|
794
|
+
border-bottom: 2px solid hsl(var(--docx-comment-hue, 45) 70% 55%);
|
|
795
|
+
cursor: pointer;
|
|
796
|
+
}
|
|
797
|
+
span.comment-highlight[data-comment-id][data-comment-resolved] { background: transparent; border-bottom-color: #cbd5e1; }
|
|
798
|
+
span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-hue, 45) 90% 78%); }
|
|
799
|
+
/* The [n] reference marker is engine chrome, not document text: hide it, keep the anchor.
|
|
800
|
+
(Qualified by the editor's roots to outrank the converter's own a.comment-marker rule,
|
|
801
|
+
which is injected later in the document.) */
|
|
802
|
+
.docx-body-flow a.comment-marker, [data-hf-band] a.comment-marker, .page-box a.comment-marker { display: none; }
|
|
803
|
+
[data-comments-hidden] span.comment-highlight[data-comment-id] { background: transparent; border-bottom-color: transparent; }
|
|
804
|
+
`;
|
|
805
|
+
//# sourceMappingURL=editor-comments.js.map
|