@erdemtuna/doc-review 0.12.0 → 0.13.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 +23 -25
- package/lib/SKILL.md +65 -202
- package/lib/agent-handoff.js +24 -0
- package/lib/agent-output.js +287 -0
- package/lib/anchor-text.js +31 -19
- package/lib/chrome-api.js +23 -4
- package/lib/chrome.html +0 -25
- package/lib/cli.js +282 -104
- package/lib/comment-target.js +4 -0
- package/lib/contracts/agent.js +122 -0
- package/lib/contracts/feedback.js +369 -1
- package/lib/contracts/frame.js +81 -0
- package/lib/contracts/history.js +189 -1
- package/lib/contracts/index.js +7 -2
- package/lib/contracts/page-boundary.js +187 -0
- package/lib/contracts/validation.js +200 -0
- package/lib/conversation-anchor-controller.js +84 -0
- package/lib/conversation-capture.js +81 -0
- package/lib/conversation-controller.js +798 -0
- package/lib/conversation-save.js +175 -0
- package/lib/conversation-server.js +184 -0
- package/lib/conversation-shell.js +1013 -0
- package/lib/conversation-store.js +809 -0
- package/lib/frame-controller.js +8 -8
- package/lib/frame-policy.js +1 -0
- package/lib/history-policy.js +4 -15
- package/lib/history-server.js +42 -326
- package/lib/html-transform.js +19 -4
- package/lib/icons.js +323 -0
- package/lib/new-message-target.js +27 -0
- package/lib/paths.js +2 -2
- package/lib/poll-transport.js +109 -26
- package/lib/positioning.js +51 -0
- package/lib/references/context-and-recovery.md +130 -0
- package/lib/references/response-contract.md +108 -0
- package/lib/references/source-edits.md +47 -0
- package/lib/revision-store.js +1 -1
- package/lib/save-controller.js +28 -14
- package/lib/sdk.js +357 -35
- package/lib/server.js +73 -595
- package/lib/setup.js +17 -48
- package/lib/state.js +21 -7
- package/lib/thread-anchor-controller.js +56 -0
- package/lib/toolbar-controller.js +3 -3
- package/lib/ui/THIRD_PARTY_NOTICES.md +127 -8
- package/lib/ui/chrome.css +1079 -2782
- package/lib/ui/chrome.js +81 -17
- package/package.json +2 -2
- package/lib/chrome-client.js +0 -1706
package/lib/sdk.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* document: editing, highlights, target resolution and serialization. It never
|
|
6
6
|
* talks to the server — everything crosses to the chrome page by postMessage.
|
|
7
7
|
*/
|
|
8
|
-
import { buildContext, findQuote } from "./anchor-text.js";
|
|
8
|
+
import { buildContext, findQuote, resolveQuote } from "./anchor-text.js";
|
|
9
|
+
import { createThreadAnchorController } from "./thread-anchor-controller.js";
|
|
9
10
|
import { hashClickAction, navigationHref } from "./click-target.js";
|
|
10
|
-
import { acceptedOpenGeneration, createHoverIntent, groupCommentTargets, nextCommentId, normalizeSelectionRange, pointInCommentApproach, sameRange, targetMessage } from "./comment-target.js";
|
|
11
|
+
import { acceptedOpenGeneration, createHoverIntent, groupCommentTargets, nextCommentId, normalizeSelectionRange, pointInCommentApproach, sameRange, sameThreadTarget, targetMessage } from "./comment-target.js";
|
|
11
12
|
import { classifyHref, externalHref, linkStyleFixup, listCommandFor, listStyleFixup, normalizeHref } from "./editing.js";
|
|
12
13
|
import { frameMessage, initializeChannelFromDocument, matchesFrameMessage } from "./frame-channel.js";
|
|
13
14
|
import { iconMarkup } from "./icons.js";
|
|
@@ -32,6 +33,7 @@ let targetGeneration = 0;
|
|
|
32
33
|
let targetObserver = null;
|
|
33
34
|
let targetIntersection = null;
|
|
34
35
|
let reviewMode = "view";
|
|
36
|
+
let canComment = false;
|
|
35
37
|
let savePolicy = "writable";
|
|
36
38
|
let modeController = null;
|
|
37
39
|
let hoverTarget = null;
|
|
@@ -41,6 +43,7 @@ let resizing = null; // live drag state while the grip is held
|
|
|
41
43
|
let suppressUntil = 0; // ignore the mouseup/click that ends a resize drag
|
|
42
44
|
let saveTimer = null;
|
|
43
45
|
let composeOpen = false;
|
|
46
|
+
let retiredComposeGeneration = 0;
|
|
44
47
|
let commentOpenRequestGeneration = null;
|
|
45
48
|
let activeCommentId = null;
|
|
46
49
|
let modeMenuOpen = false;
|
|
@@ -61,6 +64,8 @@ let themeRevision = 0;
|
|
|
61
64
|
let themeWarnings = 0;
|
|
62
65
|
const blockTargets = new Map();
|
|
63
66
|
const blockMarkers = new Map();
|
|
67
|
+
let threadAnchors = null;
|
|
68
|
+
const threadTargets = new Map();
|
|
64
69
|
/** True when the page's own scripts rewrote the DOM before any user edit. */
|
|
65
70
|
let dynamic = false;
|
|
66
71
|
const diagnostic = (event, detail = {}) => {
|
|
@@ -175,7 +180,7 @@ shadow.innerHTML = `
|
|
|
175
180
|
.active { border: 2px solid var(--review-annotation-border); }
|
|
176
181
|
.block-marker { position: fixed; pointer-events: none; z-index: 2147483644;
|
|
177
182
|
box-sizing: border-box; border-left: 3px solid var(--review-annotation-border); background: var(--review-annotation-tint); }
|
|
178
|
-
.block-marker[data-active="true"] { border-left-width:
|
|
183
|
+
.block-marker[data-active="true"] { border-left-width: 3px; background: var(--review-annotation-active-tint); }
|
|
179
184
|
.block-badge { position: fixed; z-index: 2147483647; pointer-events: auto;
|
|
180
185
|
height: 24px; min-width: 32px; box-sizing: border-box; padding: 2px 6px;
|
|
181
186
|
border: 1px solid var(--review-annotation-border); border-radius: 5px; background: var(--review-annotation-background); color: var(--review-annotation-foreground);
|
|
@@ -242,9 +247,12 @@ shadow.innerHTML = `
|
|
|
242
247
|
button:focus-visible, input:focus-visible, .mover:focus-visible, .grip:focus-visible {
|
|
243
248
|
outline: 3px solid var(--review-ring); outline-offset: 2px;
|
|
244
249
|
}
|
|
245
|
-
.
|
|
250
|
+
.chip, .grip, .hint, .linkbox, .mover, .dropline, .comment-action {
|
|
246
251
|
box-shadow: 0 0 0 1px var(--review-halo-light), 0 0 0 2px var(--review-halo-dark), 0 3px 10px var(--review-review-shadow-color);
|
|
247
252
|
}
|
|
253
|
+
.block-marker { box-shadow: 0 0 0 1px var(--review-halo-light); }
|
|
254
|
+
.block-badge { box-shadow: 0 1px 4px var(--review-review-shadow-color); }
|
|
255
|
+
.box.active { box-shadow: 0 0 0 1px var(--review-halo-light); border-width: 1px; }
|
|
248
256
|
.selection-cues { position: fixed; inset: 0; pointer-events: none; z-index: 2147483645; }
|
|
249
257
|
.selection-cue { position: fixed; border-radius: 2px; background: var(--review-annotation-tint);
|
|
250
258
|
box-shadow: inset 0 -2px var(--review-annotation-border), 0 1px var(--review-halo-light), 0 2px var(--review-halo-dark); }
|
|
@@ -267,7 +275,7 @@ shadow.innerHTML = `
|
|
|
267
275
|
<div class="dropline" id="dropline"></div>
|
|
268
276
|
<div class="selection-cues" id="selectionCues"></div>
|
|
269
277
|
<div id="blockAnnotations" role="group" aria-label="Saved block comments"></div>
|
|
270
|
-
<button class="comment-action" id="commentAction" title="Comment on this target" aria-label="Comment on this target">${iconMarkup("messageSquarePlus", { size: 17 })}</button>
|
|
278
|
+
<button class="comment-action" id="commentAction" disabled title="Comment on this target" aria-label="Comment on this target">${iconMarkup("messageSquarePlus", { size: 17 })}</button>
|
|
271
279
|
`;
|
|
272
280
|
const els = {};
|
|
273
281
|
const mountOverlay = () => {
|
|
@@ -295,7 +303,8 @@ function positionBlockBadge(badge, rect) {
|
|
|
295
303
|
const candidates = [
|
|
296
304
|
[rect.left - width - 5, rect.top], [rect.right + 5, rect.top],
|
|
297
305
|
[rect.right - width, rect.top - height - 5], [rect.right - width, rect.bottom + 5],
|
|
298
|
-
[innerWidth - width - 4,
|
|
306
|
+
[innerWidth - width - 4, Math.max(4, Math.min(innerHeight - height - 4, rect.top))],
|
|
307
|
+
[4, Math.max(4, Math.min(innerHeight - height - 4, rect.top))],
|
|
299
308
|
];
|
|
300
309
|
const controls = "a[href], button, input, select, textarea, summary, [role=button], [role=tab], [contenteditable=true]";
|
|
301
310
|
const occupied = [...blockMarkers.values()].filter((entry) => entry.badge !== badge && entry.badge.style.display !== "none")
|
|
@@ -344,7 +353,7 @@ function renderBlockAnnotations() {
|
|
|
344
353
|
const currentIds = groupCommentTargets(blockTargets).get(element) || [];
|
|
345
354
|
const id = nextCommentId(currentIds, activeCommentId);
|
|
346
355
|
if (id)
|
|
347
|
-
|
|
356
|
+
requestThreadActivation(id);
|
|
348
357
|
});
|
|
349
358
|
els.blockAnnotations.append(marker, badge);
|
|
350
359
|
entry = { marker, badge };
|
|
@@ -355,9 +364,9 @@ function renderBlockAnnotations() {
|
|
|
355
364
|
marker.dataset.active = String(active);
|
|
356
365
|
badge.setAttribute("aria-pressed", String(active));
|
|
357
366
|
const label = commentTargetFor(element)?.label || element.tagName.toLowerCase();
|
|
358
|
-
const name =
|
|
367
|
+
const name = ids.length > 1 ? `Open ${ids.length} conversations` : "Open conversation";
|
|
359
368
|
badge.setAttribute("aria-label", name);
|
|
360
|
-
badge.title = name
|
|
369
|
+
badge.title = `${name} on ${label}`;
|
|
361
370
|
badge.textContent = `◧ ${ids.length}`;
|
|
362
371
|
const rect = visibleRects([rectData(element.getBoundingClientRect())], { kind: "element", element })[0];
|
|
363
372
|
marker.style.display = badge.style.display = "none";
|
|
@@ -366,6 +375,8 @@ function renderBlockAnnotations() {
|
|
|
366
375
|
Object.assign(marker.style, { display: "block", left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
|
|
367
376
|
positionBlockBadge(badge, rect);
|
|
368
377
|
}
|
|
378
|
+
if (pending || retarget)
|
|
379
|
+
positionCommentAction(targetRects(retarget || pending));
|
|
369
380
|
refreshGeometryWatch();
|
|
370
381
|
}
|
|
371
382
|
function showMover(el) {
|
|
@@ -602,13 +613,23 @@ function renderSelectionCues(rects) {
|
|
|
602
613
|
function positionCommentAction(rects, target = retarget || pending) {
|
|
603
614
|
const visible = visibleRects(rects, target);
|
|
604
615
|
const rect = visible[visible.length - 1];
|
|
605
|
-
if (!rect || (composeOpen && !retarget)) {
|
|
616
|
+
if (!canComment || !rect || (composeOpen && !retarget)) {
|
|
617
|
+
els.commentAction.style.display = "none";
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
const x = Math.max(4, Math.min(window.innerWidth - 34, rect.right + 6));
|
|
621
|
+
const y = Math.max(4, Math.min(window.innerHeight - 34, rect.top - 4));
|
|
622
|
+
const badges = [...blockMarkers.values()].filter(({ badge }) => badge.style.display !== "none")
|
|
623
|
+
.map(({ badge }) => badge.getBoundingClientRect());
|
|
624
|
+
const position = [[x, y], [x, y + 38], [x, y - 38], [x - 38, y]].find(([left, top]) => left >= 4 && top >= 4 && left + 30 <= innerWidth - 4 && top + 30 <= innerHeight - 4 &&
|
|
625
|
+
!badges.some((badge) => left < badge.right && left + 30 > badge.left && top < badge.bottom && top + 30 > badge.top));
|
|
626
|
+
if (!position) {
|
|
606
627
|
els.commentAction.style.display = "none";
|
|
607
628
|
return false;
|
|
608
629
|
}
|
|
609
630
|
els.commentAction.style.display = "flex";
|
|
610
|
-
els.commentAction.style.left = `${
|
|
611
|
-
els.commentAction.style.top = `${
|
|
631
|
+
els.commentAction.style.left = `${position[0]}px`;
|
|
632
|
+
els.commentAction.style.top = `${position[1]}px`;
|
|
612
633
|
return true;
|
|
613
634
|
}
|
|
614
635
|
function postTarget(type, target = pending) {
|
|
@@ -714,7 +735,7 @@ function geometryWatchSignature() {
|
|
|
714
735
|
});
|
|
715
736
|
}
|
|
716
737
|
function refreshGeometryWatch() {
|
|
717
|
-
const shouldWatch = !disposed && !!(pending || retarget || activeCommentId || blockTargets.size);
|
|
738
|
+
const shouldWatch = !disposed && !!(pending || retarget || activeCommentId || blockTargets.size || threadAnchors?.projection?.anchors.length);
|
|
718
739
|
if (!shouldWatch) {
|
|
719
740
|
clearInterval(geometryWatchTimer);
|
|
720
741
|
geometryWatchTimer = null;
|
|
@@ -725,10 +746,18 @@ function refreshGeometryWatch() {
|
|
|
725
746
|
return;
|
|
726
747
|
watchedGeometrySignature = geometryWatchSignature();
|
|
727
748
|
geometryWatchTimer = setInterval(() => {
|
|
728
|
-
if (disposed || !(pending || retarget || activeCommentId || blockTargets.size)) {
|
|
749
|
+
if (disposed || !(pending || retarget || activeCommentId || blockTargets.size || threadAnchors?.projection?.anchors.length)) {
|
|
729
750
|
refreshGeometryWatch();
|
|
730
751
|
return;
|
|
731
752
|
}
|
|
753
|
+
if (threadAnchors?.projection) {
|
|
754
|
+
try {
|
|
755
|
+
threadAnchors.refresh();
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
diagnostic("thread-boundary-rejected", { message: error.message });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
732
761
|
const next = geometryWatchSignature();
|
|
733
762
|
if (next === watchedGeometrySignature)
|
|
734
763
|
return;
|
|
@@ -878,6 +907,8 @@ function cssPath(el) {
|
|
|
878
907
|
}
|
|
879
908
|
/** The heading a block sits under, used to name edits in arbitrary HTML. */
|
|
880
909
|
function precedingHeading(el) {
|
|
910
|
+
if (/^h[1-6]$/i.test(el.tagName) && el.textContent.trim())
|
|
911
|
+
return el.textContent.trim();
|
|
881
912
|
let node = el;
|
|
882
913
|
while (node && node !== document.body) {
|
|
883
914
|
let sib = node.previousElementSibling;
|
|
@@ -932,7 +963,9 @@ function targetFor(node) {
|
|
|
932
963
|
// one edit row, so number them.
|
|
933
964
|
const twins = block.parentElement ? [...block.parentElement.children].filter((c) => c.tagName === block.tagName) : [];
|
|
934
965
|
const ordinal = twins.length > 1 ? ` ${twins.indexOf(block) + 1}` : "";
|
|
935
|
-
|
|
966
|
+
const excerpt = clip(block.textContent.replace(/\s+/g, " ").trim(), 40);
|
|
967
|
+
pinnedLabels.set(block, heading ? `${clip(heading, 26)} · ${tag}${ordinal}: ${excerpt}`
|
|
968
|
+
: excerpt ? `${excerpt}${ordinal ? ` · ${tag}${ordinal}` : ""}` : `${tag}${ordinal}`);
|
|
936
969
|
}
|
|
937
970
|
return { el: block, label: pinnedLabels.get(block), authored: false };
|
|
938
971
|
}
|
|
@@ -1267,9 +1300,12 @@ function restoreTargetFocus(target) {
|
|
|
1267
1300
|
return;
|
|
1268
1301
|
if (target.kind === "element" && target.element?.isConnected) {
|
|
1269
1302
|
const element = target.element;
|
|
1303
|
+
const focused = document.activeElement;
|
|
1304
|
+
if (document.hasFocus() && focused !== document.body && focused !== element && !isOurs(focused))
|
|
1305
|
+
return;
|
|
1270
1306
|
element.focus({ preventScroll: true });
|
|
1271
1307
|
requestAnimationFrame(() => {
|
|
1272
|
-
if (element.isConnected && document.activeElement
|
|
1308
|
+
if (element.isConnected && document.activeElement === document.body) {
|
|
1273
1309
|
element.focus({ preventScroll: true });
|
|
1274
1310
|
}
|
|
1275
1311
|
});
|
|
@@ -1297,6 +1333,8 @@ function restoreTargetFocus(target) {
|
|
|
1297
1333
|
* contextual action or keyboard shortcut owns that transition.
|
|
1298
1334
|
*/
|
|
1299
1335
|
function settleSelection() {
|
|
1336
|
+
if (!canComment)
|
|
1337
|
+
return false;
|
|
1300
1338
|
const sel = document.getSelection();
|
|
1301
1339
|
if (!sel || sel.isCollapsed || !sel.rangeCount)
|
|
1302
1340
|
return false;
|
|
@@ -1347,6 +1385,8 @@ function settleSelection() {
|
|
|
1347
1385
|
return true;
|
|
1348
1386
|
}
|
|
1349
1387
|
function setElementTarget(container) {
|
|
1388
|
+
if (!canComment)
|
|
1389
|
+
return;
|
|
1350
1390
|
if (!container?.el ||
|
|
1351
1391
|
(pending && pending.kind === "element" && pending.element === container.el) ||
|
|
1352
1392
|
(retarget && retarget.kind === "element" && retarget.element === container.el)) {
|
|
@@ -1373,7 +1413,7 @@ function setElementTarget(container) {
|
|
|
1373
1413
|
}
|
|
1374
1414
|
function openPendingCompose() {
|
|
1375
1415
|
hoverIntent?.cancel();
|
|
1376
|
-
if (composing || disposed || !themeRevision)
|
|
1416
|
+
if (!canComment || composing || disposed || !themeRevision)
|
|
1377
1417
|
return false;
|
|
1378
1418
|
const selection = document.getSelection();
|
|
1379
1419
|
if (selection && !selection.isCollapsed && selection.rangeCount) {
|
|
@@ -1391,6 +1431,8 @@ function openPendingCompose() {
|
|
|
1391
1431
|
function acceptCommentOpen(msg) {
|
|
1392
1432
|
hoverIntent?.cancel();
|
|
1393
1433
|
const requested = Number(msg.requestedGeneration);
|
|
1434
|
+
if (requested <= retiredComposeGeneration)
|
|
1435
|
+
return;
|
|
1394
1436
|
if (commentOpenRequestGeneration === requested)
|
|
1395
1437
|
commentOpenRequestGeneration = null;
|
|
1396
1438
|
if (!msg.accepted) {
|
|
@@ -1535,7 +1577,8 @@ function reanchor(comments) {
|
|
|
1535
1577
|
let el = blockTargets.get(comment.id);
|
|
1536
1578
|
if (!el?.isConnected) {
|
|
1537
1579
|
try {
|
|
1538
|
-
|
|
1580
|
+
const matches = comment.anchor?.selector ? [...document.querySelectorAll(comment.anchor.selector)].filter((element) => !isOurs(element)) : [];
|
|
1581
|
+
el = matches.length === 1 ? matches[0] : null;
|
|
1539
1582
|
}
|
|
1540
1583
|
catch {
|
|
1541
1584
|
el = null;
|
|
@@ -1559,9 +1602,145 @@ function reanchor(comments) {
|
|
|
1559
1602
|
const marks = wrapOffsets(map, hit.start, hit.end, comment.id);
|
|
1560
1603
|
(marks.length ? resolved : orphaned).push(comment.id);
|
|
1561
1604
|
}
|
|
1562
|
-
|
|
1605
|
+
if (!threadAnchors?.projection)
|
|
1606
|
+
post("eh:anchorStatus", { resolved, orphaned });
|
|
1563
1607
|
renderBlockAnnotations();
|
|
1564
1608
|
}
|
|
1609
|
+
function hiddenThreadTarget(element) {
|
|
1610
|
+
for (let node = element; node; node = node.parentElement) {
|
|
1611
|
+
const style = getComputedStyle(node);
|
|
1612
|
+
if (node.hidden || node.getAttribute("aria-hidden") === "true" || style.display === "none" ||
|
|
1613
|
+
style.visibility === "hidden" || style.visibility === "collapse" ||
|
|
1614
|
+
style.contentVisibility === "hidden" || Number(style.opacity) === 0)
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
return false;
|
|
1618
|
+
}
|
|
1619
|
+
const threadElements = new Map();
|
|
1620
|
+
function resolveThreadAnchor({ threadId, target }) {
|
|
1621
|
+
threadTargets.delete(threadId);
|
|
1622
|
+
const unavailable = (reason) => ({ threadId, state: "unavailable", reason });
|
|
1623
|
+
if (!document.body || document.readyState === "loading")
|
|
1624
|
+
return unavailable("render-loading");
|
|
1625
|
+
let resolved;
|
|
1626
|
+
if (target.kind === "element") {
|
|
1627
|
+
let candidates;
|
|
1628
|
+
try {
|
|
1629
|
+
candidates = [...document.querySelectorAll(target.anchor.selector)]
|
|
1630
|
+
.filter((element) => document.body.contains(element) && !isOurs(element));
|
|
1631
|
+
}
|
|
1632
|
+
catch (error) {
|
|
1633
|
+
if (error.name !== "SyntaxError")
|
|
1634
|
+
throw error;
|
|
1635
|
+
return unavailable("invalid-selector");
|
|
1636
|
+
}
|
|
1637
|
+
if (!candidates.length)
|
|
1638
|
+
return { threadId, state: "missing" };
|
|
1639
|
+
if (candidates.length > 1)
|
|
1640
|
+
return { threadId, state: "ambiguous", candidateCount: candidates.length };
|
|
1641
|
+
const previous = threadElements.get(threadId);
|
|
1642
|
+
if (previous && previous !== candidates[0])
|
|
1643
|
+
return unavailable("render-changed");
|
|
1644
|
+
threadElements.set(threadId, candidates[0]);
|
|
1645
|
+
if (hiddenThreadTarget(candidates[0]))
|
|
1646
|
+
return unavailable("hidden");
|
|
1647
|
+
resolved = { kind: "element", element: candidates[0] };
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
const { text, map } = flatten();
|
|
1651
|
+
const hit = resolveQuote(text, target.anchor);
|
|
1652
|
+
if (hit.state !== "found")
|
|
1653
|
+
return { threadId, ...hit };
|
|
1654
|
+
const entries = map.filter((entry) => entry.end > hit.start && entry.start < hit.end);
|
|
1655
|
+
if (!entries.length)
|
|
1656
|
+
return unavailable("not-measurable");
|
|
1657
|
+
if (entries.some((entry) => hiddenThreadTarget(entry.node.parentElement)))
|
|
1658
|
+
return unavailable("hidden");
|
|
1659
|
+
const range = document.createRange();
|
|
1660
|
+
range.setStart(entries[0].node, hit.start - entries[0].start);
|
|
1661
|
+
range.setEnd(entries.at(-1).node, hit.end - entries.at(-1).start);
|
|
1662
|
+
resolved = { kind: "selection", range };
|
|
1663
|
+
}
|
|
1664
|
+
const raw = targetRects(resolved).filter((rect) => rect.width > 0 && rect.height > 0);
|
|
1665
|
+
if (!raw.length)
|
|
1666
|
+
return unavailable("not-measurable");
|
|
1667
|
+
const viewport = viewportData();
|
|
1668
|
+
const relationTo = (clip) => raw.every((rect) => rect.bottom <= clip.top) ? "above"
|
|
1669
|
+
: raw.every((rect) => rect.top >= clip.bottom) ? "below"
|
|
1670
|
+
: raw.every((rect) => rect.right <= clip.left) ? "left"
|
|
1671
|
+
: raw.every((rect) => rect.left >= clip.right) ? "right" : "visible";
|
|
1672
|
+
const viewportRelation = relationTo({ left: 0, top: 0, right: viewport.width, bottom: viewport.height });
|
|
1673
|
+
const clip = effectiveClipRect(resolved);
|
|
1674
|
+
const relation = viewportRelation !== "visible" ? viewportRelation : clip ? relationTo(clip) : null;
|
|
1675
|
+
if (!relation)
|
|
1676
|
+
return unavailable("not-measurable");
|
|
1677
|
+
const rects = relation === "visible" ? raw.map((rect) => intersectRects(rect, clip)).filter(Boolean) : raw;
|
|
1678
|
+
if (!rects.length)
|
|
1679
|
+
return unavailable("not-measurable");
|
|
1680
|
+
threadTargets.set(threadId, resolved);
|
|
1681
|
+
return { threadId, state: "found", rects, viewport, relation };
|
|
1682
|
+
}
|
|
1683
|
+
threadAnchors = createThreadAnchorController({
|
|
1684
|
+
channel: frameMessage("scope"),
|
|
1685
|
+
resolve: resolveThreadAnchor,
|
|
1686
|
+
reconcile(projection, states) {
|
|
1687
|
+
const open = new Set(projection.anchors.filter(anchor => !anchor.resolved).map(anchor => anchor.threadId));
|
|
1688
|
+
const found = new Set(states.anchors.filter((anchor) => anchor.state === "found" && open.has(anchor.threadId)).map((anchor) => anchor.threadId));
|
|
1689
|
+
for (const threadId of found) {
|
|
1690
|
+
const target = threadTargets.get(threadId);
|
|
1691
|
+
if (target.kind === "element")
|
|
1692
|
+
blockTargets.set(threadId, target.element);
|
|
1693
|
+
else {
|
|
1694
|
+
const marks = marksFor(threadId);
|
|
1695
|
+
if (marks.length && (!marks.some((mark) => mark.contains(target.range.startContainer)) ||
|
|
1696
|
+
!marks.some((mark) => mark.contains(target.range.endContainer)) ||
|
|
1697
|
+
marks.map((mark) => mark.textContent).join("") !== target.range.toString()))
|
|
1698
|
+
unwrap(threadId);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
reanchor(projection.anchors.filter((anchor) => found.has(anchor.threadId)).map(({ threadId, target }) => ({
|
|
1702
|
+
id: threadId, kind: target.kind, anchor: target.anchor,
|
|
1703
|
+
})));
|
|
1704
|
+
for (const threadId of found) {
|
|
1705
|
+
const state = states.anchors.find((item) => item.threadId === threadId);
|
|
1706
|
+
const peers = states.anchors.filter((item) => found.has(item.threadId) && sameThreadTarget(item, state)).map((item) => item.threadId);
|
|
1707
|
+
const marks = marksFor(threadId);
|
|
1708
|
+
for (const [index, mark] of marks.entries()) {
|
|
1709
|
+
const nestedPeer = [...mark.querySelectorAll(`mark[${MARK_ATTR}]`)].some((nested) => peers.includes(nested.getAttribute(MARK_ATTR)));
|
|
1710
|
+
const interactive = index === 0 && !nestedPeer;
|
|
1711
|
+
if (mark.tabIndex !== (interactive ? 0 : -1))
|
|
1712
|
+
mark.tabIndex = interactive ? 0 : -1;
|
|
1713
|
+
const role = interactive ? "button" : "presentation";
|
|
1714
|
+
if (mark.getAttribute("role") !== role)
|
|
1715
|
+
mark.setAttribute("role", role);
|
|
1716
|
+
const label = peers.length > 1 ? `Open ${peers.length} conversations` : "Open conversation";
|
|
1717
|
+
if (interactive && mark.getAttribute("aria-label") !== label)
|
|
1718
|
+
mark.setAttribute("aria-label", label);
|
|
1719
|
+
if (!interactive && mark.hasAttribute("aria-label"))
|
|
1720
|
+
mark.removeAttribute("aria-label");
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
1724
|
+
changed(_projection, states) {
|
|
1725
|
+
post("eh:threadAnchorStates", states);
|
|
1726
|
+
},
|
|
1727
|
+
});
|
|
1728
|
+
function threadAction(action, threadId) {
|
|
1729
|
+
const { anchors: _anchors, type: _type, ...scope } = threadAnchors.projection;
|
|
1730
|
+
return threadAnchors.action({ type: "eh:threadAction", ...scope, action, threadId });
|
|
1731
|
+
}
|
|
1732
|
+
function requestThreadActivation(id) {
|
|
1733
|
+
if (!threadAnchors.projection) {
|
|
1734
|
+
post("eh:activate", { id });
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
post("eh:threadAction", threadAction("activate", id));
|
|
1739
|
+
}
|
|
1740
|
+
catch (error) {
|
|
1741
|
+
diagnostic("thread-boundary-rejected", { message: error.message });
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1565
1744
|
function activate(id, scroll) {
|
|
1566
1745
|
activeCommentId = id || null;
|
|
1567
1746
|
const marks = marksFor(id);
|
|
@@ -1627,17 +1806,43 @@ const REVIEW_DOCUMENT_COLORS = {
|
|
|
1627
1806
|
mark[${MARK_ATTR}] { background: #FFF1D9; color: #75470E; text-decoration: underline #915B13; }
|
|
1628
1807
|
mark[${MARK_ATTR}]:hover, mark[${MARK_ATTR}].eh-active { background: #F5D6A3; }
|
|
1629
1808
|
mark[${MARK_ATTR}].eh-active { outline: 2px solid #915B13; outline-offset: 1px; }
|
|
1809
|
+
body[contenteditable="true"] mark[${MARK_ATTR}] { color: inherit; background: transparent; text-decoration: none; outline: 1px dashed #915B13; outline-offset: 1px; }
|
|
1810
|
+
body[contenteditable="true"] mark[${MARK_ATTR}].eh-active { outline: 2px solid #915B13; }
|
|
1630
1811
|
::selection { background: #F5D6A3; color: #75470E; }
|
|
1631
1812
|
`,
|
|
1632
1813
|
dark: `
|
|
1633
1814
|
mark[${MARK_ATTR}] { background: #3D3020; color: #F1CC8E; text-decoration: underline #E7BB72; }
|
|
1634
1815
|
mark[${MARK_ATTR}]:hover, mark[${MARK_ATTR}].eh-active { background: #594325; }
|
|
1635
1816
|
mark[${MARK_ATTR}].eh-active { outline: 2px solid #E7BB72; outline-offset: 1px; }
|
|
1817
|
+
body[contenteditable="true"] mark[${MARK_ATTR}] { color: inherit; background: transparent; text-decoration: none; outline: 1px dashed #E7BB72; outline-offset: 1px; }
|
|
1818
|
+
body[contenteditable="true"] mark[${MARK_ATTR}].eh-active { outline: 2px solid #E7BB72; }
|
|
1636
1819
|
::selection { background: #594325; color: #F1CC8E; }
|
|
1637
1820
|
`,
|
|
1638
1821
|
};
|
|
1639
1822
|
/* REVIEW_THEME_DOCUMENT_END */
|
|
1640
1823
|
function boot() {
|
|
1824
|
+
document.addEventListener("keydown", (event) => {
|
|
1825
|
+
if (!threadAnchors.projection || event.isComposing || event.keyCode === 229)
|
|
1826
|
+
return;
|
|
1827
|
+
const mark = event.target.closest?.(`mark[${MARK_ATTR}]`);
|
|
1828
|
+
if (mark && (event.key === "Enter" || event.key === " ")) {
|
|
1829
|
+
event.preventDefault();
|
|
1830
|
+
event.stopPropagation();
|
|
1831
|
+
requestThreadActivation(mark.getAttribute(MARK_ATTR));
|
|
1832
|
+
}
|
|
1833
|
+
else if (event.key === "Escape" && activeCommentId &&
|
|
1834
|
+
(isOurs(event.target) || !event.target.closest?.("input,textarea,select,[contenteditable='true']"))) {
|
|
1835
|
+
try {
|
|
1836
|
+
post("eh:threadAction", threadAction("dismiss", activeCommentId));
|
|
1837
|
+
deactivateComment();
|
|
1838
|
+
event.preventDefault();
|
|
1839
|
+
event.stopPropagation();
|
|
1840
|
+
}
|
|
1841
|
+
catch (error) {
|
|
1842
|
+
diagnostic("thread-boundary-rejected", { message: error.message });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1641
1846
|
mountOverlay();
|
|
1642
1847
|
const style = document.createElement("style");
|
|
1643
1848
|
style.setAttribute("data-eh-sdk", "");
|
|
@@ -1709,7 +1914,7 @@ function boot() {
|
|
|
1709
1914
|
const mark = event.target.closest && event.target.closest(`mark[${MARK_ATTR}]`);
|
|
1710
1915
|
if (mark && themeRevision) {
|
|
1711
1916
|
event.preventDefault();
|
|
1712
|
-
|
|
1917
|
+
requestThreadActivation(mark.getAttribute(MARK_ATTR));
|
|
1713
1918
|
return;
|
|
1714
1919
|
}
|
|
1715
1920
|
if (reviewMode === "view") {
|
|
@@ -2000,20 +2205,23 @@ function boot() {
|
|
|
2000
2205
|
const target = targetFor(hoverTarget);
|
|
2001
2206
|
const label = target ? target.label : "Element";
|
|
2002
2207
|
const before = hoverTarget.textContent;
|
|
2208
|
+
const before_html = blockHtml(hoverTarget);
|
|
2003
2209
|
hoverTarget.remove();
|
|
2004
2210
|
hoverTarget = null;
|
|
2005
2211
|
place(els.outline, null);
|
|
2006
2212
|
showChip(null);
|
|
2007
|
-
queueEdit({ label, kind: "deleted", before, after: "" });
|
|
2213
|
+
queueEdit({ label, kind: "deleted", before, before_html, after: "" });
|
|
2008
2214
|
flushSave();
|
|
2009
2215
|
});
|
|
2010
2216
|
// beforeinput still sees the untouched wording, so capture it once per block.
|
|
2011
2217
|
const originalText = new WeakMap();
|
|
2012
2218
|
const originalHtml = new WeakMap();
|
|
2219
|
+
const capturedBlocks = new Set();
|
|
2013
2220
|
const captureOriginal = (el) => {
|
|
2014
2221
|
if (!originalText.has(el)) {
|
|
2015
2222
|
originalText.set(el, el.textContent);
|
|
2016
2223
|
originalHtml.set(el, blockHtml(el));
|
|
2224
|
+
capturedBlocks.add(el);
|
|
2017
2225
|
}
|
|
2018
2226
|
};
|
|
2019
2227
|
/** An edit row for a block changed outside the input-event flow (attribute
|
|
@@ -2269,6 +2477,11 @@ function boot() {
|
|
|
2269
2477
|
event.key.toLowerCase() === "m") {
|
|
2270
2478
|
if (selectionIsActive())
|
|
2271
2479
|
settleSelection();
|
|
2480
|
+
else if (event.target !== document.body && event.target !== document.documentElement) {
|
|
2481
|
+
const target = commentTargetFor(event.target);
|
|
2482
|
+
if (target)
|
|
2483
|
+
setElementTarget(target);
|
|
2484
|
+
}
|
|
2272
2485
|
if (openPendingCompose()) {
|
|
2273
2486
|
event.preventDefault();
|
|
2274
2487
|
event.stopPropagation();
|
|
@@ -2463,6 +2676,16 @@ function boot() {
|
|
|
2463
2676
|
// not HTML5 drag: the element is relocated on drop, never cloned, so
|
|
2464
2677
|
// identity (labels, captured originals, comment anchors) survives the move.
|
|
2465
2678
|
let moving = null; // { el, label, drop: { ref, before } | null } while the handle is held
|
|
2679
|
+
const cancelMove = () => {
|
|
2680
|
+
if (!moving)
|
|
2681
|
+
return;
|
|
2682
|
+
moving.preview.cancel();
|
|
2683
|
+
moving = null;
|
|
2684
|
+
showDropline(null);
|
|
2685
|
+
showMover(null);
|
|
2686
|
+
};
|
|
2687
|
+
window.addEventListener("pointercancel", cancelMove);
|
|
2688
|
+
window.addEventListener("blur", cancelMove);
|
|
2466
2689
|
const dropPointFor = (x, y) => {
|
|
2467
2690
|
const under = document.elementFromPoint(x, y);
|
|
2468
2691
|
if (!under || isOurs(under))
|
|
@@ -2482,9 +2705,12 @@ function boot() {
|
|
|
2482
2705
|
event.preventDefault();
|
|
2483
2706
|
event.stopPropagation();
|
|
2484
2707
|
const target = targetFor(el);
|
|
2485
|
-
moving = {
|
|
2708
|
+
moving = {
|
|
2709
|
+
el, label: target ? target.label : "Block", drop: null,
|
|
2710
|
+
before: el.textContent, beforeHtml: blockHtml(el),
|
|
2711
|
+
preview: el.animate([{ opacity: 0.4 }, { opacity: 0.4 }], { duration: 1000, iterations: Infinity }),
|
|
2712
|
+
};
|
|
2486
2713
|
captureOriginal(moving.el);
|
|
2487
|
-
moving.el.style.opacity = "0.4";
|
|
2488
2714
|
place(els.outline, null);
|
|
2489
2715
|
showChip(null);
|
|
2490
2716
|
try {
|
|
@@ -2504,11 +2730,11 @@ function boot() {
|
|
|
2504
2730
|
window.addEventListener("pointerup", () => {
|
|
2505
2731
|
if (!moving)
|
|
2506
2732
|
return;
|
|
2507
|
-
const { el, label, drop } = moving;
|
|
2733
|
+
const { el, label, drop, before, beforeHtml, preview } = moving;
|
|
2508
2734
|
moving = null;
|
|
2509
2735
|
showDropline(null);
|
|
2510
2736
|
showMover(null);
|
|
2511
|
-
|
|
2737
|
+
preview.cancel();
|
|
2512
2738
|
suppressUntil = Date.now() + 250;
|
|
2513
2739
|
if (!drop || !drop.ref.isConnected || !el.isConnected)
|
|
2514
2740
|
return;
|
|
@@ -2523,9 +2749,9 @@ function boot() {
|
|
|
2523
2749
|
queueEdit({
|
|
2524
2750
|
label,
|
|
2525
2751
|
kind: "moved",
|
|
2526
|
-
before
|
|
2752
|
+
before,
|
|
2527
2753
|
after: el.textContent,
|
|
2528
|
-
before_html:
|
|
2754
|
+
before_html: beforeHtml,
|
|
2529
2755
|
after_html: blockHtml(el),
|
|
2530
2756
|
moved_after: prev ? clip(prev.textContent, 90) : "",
|
|
2531
2757
|
moved_before: following ? clip(following.textContent, 90) : "",
|
|
@@ -2643,9 +2869,44 @@ function boot() {
|
|
|
2643
2869
|
reposition();
|
|
2644
2870
|
});
|
|
2645
2871
|
};
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2872
|
+
let readingAnchor = null;
|
|
2873
|
+
let readingWidth = innerWidth;
|
|
2874
|
+
const rememberPassage = () => {
|
|
2875
|
+
const selection = window.getSelection();
|
|
2876
|
+
let range = selection?.rangeCount && !selection.isCollapsed ? selection.getRangeAt(0).cloneRange() : null;
|
|
2877
|
+
const selected = range?.getBoundingClientRect();
|
|
2878
|
+
if (!selected || selected.bottom <= 0 || selected.top >= innerHeight) {
|
|
2879
|
+
range = document.caretRangeFromPoint?.(innerWidth / 2, innerHeight / 2) ?? null;
|
|
2880
|
+
}
|
|
2881
|
+
if (!range || range.startContainer.getRootNode() !== document || !range.startContainer.isConnected) {
|
|
2882
|
+
readingAnchor = null;
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const rect = range.getBoundingClientRect();
|
|
2886
|
+
readingAnchor = rect.height ? { range, top: rect.top } : null;
|
|
2887
|
+
readingWidth = innerWidth;
|
|
2888
|
+
};
|
|
2889
|
+
const scrolled = () => {
|
|
2890
|
+
scheduleReposition();
|
|
2891
|
+
// A resize-generated scroll must not replace the anchor before restoration.
|
|
2892
|
+
if (innerWidth === readingWidth)
|
|
2893
|
+
rememberPassage();
|
|
2894
|
+
};
|
|
2895
|
+
document.addEventListener("scroll", scrolled, true);
|
|
2896
|
+
window.addEventListener("scroll", scrolled, true);
|
|
2897
|
+
document.addEventListener("selectionchange", () => { if (innerWidth === readingWidth)
|
|
2898
|
+
rememberPassage(); });
|
|
2899
|
+
window.addEventListener("resize", () => {
|
|
2900
|
+
if (innerWidth !== readingWidth && readingAnchor?.range.startContainer.isConnected) {
|
|
2901
|
+
const delta = readingAnchor.range.getBoundingClientRect().top - readingAnchor.top;
|
|
2902
|
+
if (Number.isFinite(delta))
|
|
2903
|
+
window.scrollBy({ top: delta, behavior: "instant" });
|
|
2904
|
+
}
|
|
2905
|
+
readingWidth = innerWidth;
|
|
2906
|
+
rememberPassage();
|
|
2907
|
+
scheduleReposition();
|
|
2908
|
+
});
|
|
2909
|
+
rememberPassage();
|
|
2649
2910
|
let scrollQueued = false;
|
|
2650
2911
|
window.addEventListener("scroll", () => {
|
|
2651
2912
|
if (scrollQueued)
|
|
@@ -2665,11 +2926,40 @@ function boot() {
|
|
|
2665
2926
|
if (!matchesFrameMessage(msg))
|
|
2666
2927
|
return;
|
|
2667
2928
|
switch (msg.type) {
|
|
2929
|
+
case "eh:threadAnchors":
|
|
2930
|
+
try {
|
|
2931
|
+
threadAnchors.project(msg);
|
|
2932
|
+
refreshGeometryWatch();
|
|
2933
|
+
}
|
|
2934
|
+
catch (error) {
|
|
2935
|
+
diagnostic("thread-boundary-rejected", { message: error.message });
|
|
2936
|
+
}
|
|
2937
|
+
break;
|
|
2938
|
+
case "eh:threadAction":
|
|
2939
|
+
try {
|
|
2940
|
+
const action = threadAnchors.action(msg);
|
|
2941
|
+
if (action.action === "dismiss")
|
|
2942
|
+
deactivateComment();
|
|
2943
|
+
else {
|
|
2944
|
+
if (action.action === "reveal")
|
|
2945
|
+
targetElement(threadTargets.get(action.threadId))?.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
|
|
2946
|
+
if (threadAnchors.projection.anchors.find(anchor => anchor.threadId === action.threadId)?.resolved)
|
|
2947
|
+
deactivateComment();
|
|
2948
|
+
else
|
|
2949
|
+
activate(action.threadId, false);
|
|
2950
|
+
threadAnchors.refresh();
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
catch (error) {
|
|
2954
|
+
diagnostic("thread-boundary-rejected", { message: error.message });
|
|
2955
|
+
}
|
|
2956
|
+
break;
|
|
2668
2957
|
case "eh:setTheme":
|
|
2669
2958
|
applyReviewTheme(msg);
|
|
2670
2959
|
break;
|
|
2671
2960
|
case "eh:anchors":
|
|
2672
|
-
|
|
2961
|
+
if (!threadAnchors.projection)
|
|
2962
|
+
reanchor(msg.comments || []);
|
|
2673
2963
|
break;
|
|
2674
2964
|
case "eh:commit":
|
|
2675
2965
|
if (msg.targetGeneration && pending && msg.targetGeneration !== pending.generation)
|
|
@@ -2681,7 +2971,15 @@ function boot() {
|
|
|
2681
2971
|
if (msg.targetGeneration && pending && msg.targetGeneration !== pending.generation)
|
|
2682
2972
|
break;
|
|
2683
2973
|
{
|
|
2974
|
+
// Cancellation can arrive before the new selection's debounced update.
|
|
2975
|
+
const selection = document.getSelection();
|
|
2976
|
+
if (selection?.rangeCount && !selection.isCollapsed &&
|
|
2977
|
+
(pending?.kind !== "selection" || !sameRange(selection.getRangeAt(0), pending.range)))
|
|
2978
|
+
settleSelection();
|
|
2684
2979
|
const discardThrough = Number(msg.discardThroughGeneration) || Number(msg.targetGeneration) || 0;
|
|
2980
|
+
retiredComposeGeneration = Math.max(retiredComposeGeneration, discardThrough);
|
|
2981
|
+
if (commentOpenRequestGeneration <= discardThrough)
|
|
2982
|
+
commentOpenRequestGeneration = null;
|
|
2685
2983
|
const newerRetarget = retarget && retarget.generation > discardThrough ? retarget : null;
|
|
2686
2984
|
if (msg.restoreFocus && !newerRetarget)
|
|
2687
2985
|
restoreTargetFocus(pending);
|
|
@@ -2719,7 +3017,8 @@ function boot() {
|
|
|
2719
3017
|
renderBlockAnnotations();
|
|
2720
3018
|
break;
|
|
2721
3019
|
case "eh:activate":
|
|
2722
|
-
|
|
3020
|
+
if (!threadAnchors.projection)
|
|
3021
|
+
activate(msg.id, !!msg.scroll);
|
|
2723
3022
|
break;
|
|
2724
3023
|
case "eh:flush":
|
|
2725
3024
|
flushSave();
|
|
@@ -2748,9 +3047,20 @@ function boot() {
|
|
|
2748
3047
|
checkDynamic(String(msg.html || ""));
|
|
2749
3048
|
break;
|
|
2750
3049
|
case "eh:configureReview": {
|
|
3050
|
+
if ((msg.mode !== "edit" && msg.mode !== "view") ||
|
|
3051
|
+
(msg.savePolicy !== "writable" && msg.savePolicy !== "feedback-only") || typeof msg.canComment !== "boolean") {
|
|
3052
|
+
console.warn("[doc-review-frame] Rejected invalid review configuration.");
|
|
3053
|
+
break;
|
|
3054
|
+
}
|
|
2751
3055
|
cancelHover();
|
|
2752
|
-
reviewMode = msg.mode
|
|
2753
|
-
savePolicy = msg.savePolicy
|
|
3056
|
+
reviewMode = msg.mode;
|
|
3057
|
+
savePolicy = msg.savePolicy;
|
|
3058
|
+
canComment = msg.canComment;
|
|
3059
|
+
els.commentAction.disabled = !canComment;
|
|
3060
|
+
if (!canComment) {
|
|
3061
|
+
clearTimeout(selectionTimer);
|
|
3062
|
+
clearPending();
|
|
3063
|
+
}
|
|
2754
3064
|
modeController.setMode(reviewMode);
|
|
2755
3065
|
if (reviewMode === "view") {
|
|
2756
3066
|
showChip(null);
|
|
@@ -2759,7 +3069,7 @@ function boot() {
|
|
|
2759
3069
|
els.linkbox.style.display = "none";
|
|
2760
3070
|
}
|
|
2761
3071
|
scheduleTargetGeometry();
|
|
2762
|
-
post("eh:configurationApplied", { mode: reviewMode, savePolicy });
|
|
3072
|
+
post("eh:configurationApplied", { mode: reviewMode, savePolicy, canComment });
|
|
2763
3073
|
break;
|
|
2764
3074
|
}
|
|
2765
3075
|
case "eh:restoreScroll":
|
|
@@ -2771,6 +3081,18 @@ function boot() {
|
|
|
2771
3081
|
case "eh:assetFailed":
|
|
2772
3082
|
pendingPastes.delete(msg.id);
|
|
2773
3083
|
break;
|
|
3084
|
+
case "eh:submittedEdits": {
|
|
3085
|
+
if (!Array.isArray(msg.labels) || msg.labels.some((label) => typeof label !== "string"))
|
|
3086
|
+
break;
|
|
3087
|
+
const labels = new Set(msg.labels);
|
|
3088
|
+
for (const element of capturedBlocks)
|
|
3089
|
+
if (labels.has(pinnedLabels.get(element))) {
|
|
3090
|
+
originalText.delete(element);
|
|
3091
|
+
originalHtml.delete(element);
|
|
3092
|
+
capturedBlocks.delete(element);
|
|
3093
|
+
}
|
|
3094
|
+
break;
|
|
3095
|
+
}
|
|
2774
3096
|
default:
|
|
2775
3097
|
break;
|
|
2776
3098
|
}
|