@plannotator/ui 0.29.1 → 0.31.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 +1 -0
- package/components/ActionMenu.tsx +6 -1
- package/components/AgentsTab.tsx +8 -9
- package/components/AnalysisLayerToggle.tsx +48 -0
- package/components/AnnotationPanel.tsx +68 -35
- package/components/AnnotationToolbar.tsx +28 -8
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/CommentPopover.tsx +212 -46
- package/components/ConfirmDialog.tsx +42 -28
- package/components/InlineMarkdown.tsx +3 -2
- package/components/KeyboardShortcuts.tsx +9 -0
- package/components/Landing.tsx +1 -1
- package/components/LookAndFeelAnnouncementDialog.tsx +147 -178
- package/components/MarkdownEditor/embedPicker.ts +349 -0
- package/components/MarkdownEditor.tsx +12 -0
- package/components/ModeToggle.tsx +2 -1
- package/components/PermissionModeSetup.tsx +24 -5
- package/components/PinpointOverlay.tsx +11 -6
- package/components/PlanHeaderMenu.tsx +140 -1
- package/components/SearchableSelect.tsx +2 -0
- package/components/Settings.tsx +136 -10
- package/components/SkillReferenceMenu.tsx +9 -0
- package/components/StickyHeaderLane.tsx +9 -2
- package/components/TableOfContents.tsx +9 -4
- package/components/TextShimmer.tsx +8 -5
- package/components/ThemeProvider.tsx +43 -1
- package/components/ThemeTab.tsx +52 -1
- package/components/Tooltip.tsx +3 -1
- package/components/Viewer.tsx +19 -5
- package/components/VimTargetReticle.tsx +12 -4
- package/components/ai/DocumentAIChatPanel.tsx +1 -0
- package/components/core/button.tsx +14 -6
- package/components/html-viewer/HtmlViewer.tsx +215 -39
- package/components/html-viewer/bridge-script.ts +384 -66
- package/components/html-viewer/composerYield.ts +1 -51
- package/components/html-viewer/useHtmlAnnotation.ts +179 -58
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -0
- package/components/sidebar/FileBrowser.tsx +17 -5
- package/components/sidebar/SidebarContainer.tsx +124 -28
- package/components/ui/button.tsx +10 -8
- package/components/ui/dialog.tsx +35 -25
- package/config/index.ts +6 -1
- package/config/reviewView.ts +42 -9
- package/config/settings.ts +141 -0
- package/hooks/useAIProviderConfig.ts +8 -7
- package/hooks/useActiveSection.ts +6 -4
- package/hooks/useAgentJobs.ts +3 -0
- package/hooks/useAnnotationHighlighter.ts +20 -0
- package/hooks/useIsMobile.ts +37 -0
- package/hooks/useLinkedDoc.ts +7 -0
- package/hooks/useScrollViewport.ts +74 -0
- package/hooks/useViewportEnvironment.ts +350 -0
- package/package.json +3 -2
- package/shortcuts/index.ts +3 -0
- package/shortcuts/plan-review/annotationMode.shortcuts.ts +91 -0
- package/shortcuts/plan-review/documentView.shortcuts.ts +26 -0
- package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +24 -0
- package/styles.css +1 -1
- package/theme.css +229 -0
- package/types.ts +34 -0
- package/utils/annotateAgentTerminal.ts +36 -5
- package/utils/blockTargeting.ts +6 -3
- package/utils/composerYield.ts +45 -0
- package/utils/htmlChrome.ts +20 -16
- package/utils/lookAndFeelAnnouncement.ts +12 -8
- package/utils/markdownExtensions.ts +57 -0
- package/utils/parser.ts +37 -2
- package/utils/vimNavigation.ts +4 -1
- package/utils/vimScroll.ts +9 -4
- package/utils/wideMode.ts +20 -0
- package/components/PlanAIAnnouncementDialog.tsx +0 -187
- package/components/VimModeAnnouncementDialog.tsx +0 -557
- package/utils/planAIAnnouncement.ts +0 -17
- package/utils/vimModeAnnouncement.ts +0 -23
|
@@ -213,19 +213,83 @@ body[data-plannotator-vim-focus-owner]:focus {
|
|
|
213
213
|
export const BRIDGE_SCRIPT = `(function() {
|
|
214
214
|
var PREFIX = 'plannotator-bridge-';
|
|
215
215
|
|
|
216
|
+
// --- Live mode (proxied local app) ---
|
|
217
|
+
// Srcdoc sessions carry no config: LIVE stays null and every branch below is
|
|
218
|
+
// inert, keeping srcdoc behavior byte-for-byte identical. The proxy injects
|
|
219
|
+
// this script into EVERY HTML response, so a frame gate deactivates the
|
|
220
|
+
// bridge when the proxied page is opened directly (not framed) and inside
|
|
221
|
+
// nested same-origin subframes: only the frame whose parent IS the editor
|
|
222
|
+
// may run.
|
|
223
|
+
var LIVE = window.__plannotatorLiveConfig || null;
|
|
224
|
+
if (LIVE && (window === window.parent || window.parent !== window.top)) return;
|
|
225
|
+
// The server cannot know which origin form (localhost or 127.0.0.1) the
|
|
226
|
+
// editor tab was opened on, so live outbound messages are posted once per
|
|
227
|
+
// listed editor origin: the browser delivers only the post whose
|
|
228
|
+
// targetOrigin matches the parent document and silently drops the rest,
|
|
229
|
+
// so exactly one copy arrives. Inbound accepts any listed origin. Srcdoc
|
|
230
|
+
// keeps targetOrigin '*' and no token.
|
|
231
|
+
function isEditorOrigin(origin) {
|
|
232
|
+
if (!LIVE) return true;
|
|
233
|
+
var list = LIVE.editorOrigins || [];
|
|
234
|
+
for (var i = 0; i < list.length; i++) { if (list[i] === origin) return true; }
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
function postToParent(msg) {
|
|
238
|
+
if (LIVE) {
|
|
239
|
+
msg.token = LIVE.token;
|
|
240
|
+
var origins = LIVE.editorOrigins || [];
|
|
241
|
+
for (var o = 0; o < origins.length; o++) parent.postMessage(msg, origins[o]);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
parent.postMessage(msg, '*');
|
|
245
|
+
}
|
|
246
|
+
// Page identity for multi-page live sessions: annotations are stamped with
|
|
247
|
+
// the page they were made on, and restore filters to the current page.
|
|
248
|
+
function currentPageUrl() {
|
|
249
|
+
return (location.pathname + location.search).slice(0, 2048);
|
|
250
|
+
}
|
|
251
|
+
if (LIVE) {
|
|
252
|
+
// SPA navigation: report history changes so the parent can re-filter the
|
|
253
|
+
// restored set. Coalesced with a microtask flag so a pushState burst posts
|
|
254
|
+
// once. Full reloads need nothing: the proxy re-injects and the fresh
|
|
255
|
+
// document posts ready again.
|
|
256
|
+
var pageChangeQueued = false;
|
|
257
|
+
var postPageChange = function() {
|
|
258
|
+
if (pageChangeQueued) return;
|
|
259
|
+
pageChangeQueued = true;
|
|
260
|
+
Promise.resolve().then(function() {
|
|
261
|
+
pageChangeQueued = false;
|
|
262
|
+
postToParent({ type: PREFIX + 'page-change', pageUrl: currentPageUrl() });
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
var wrapHistory = function(name) {
|
|
266
|
+
var original = history[name];
|
|
267
|
+
if (typeof original !== 'function') return;
|
|
268
|
+
history[name] = function() {
|
|
269
|
+
var result = original.apply(this, arguments);
|
|
270
|
+
postPageChange();
|
|
271
|
+
return result;
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
wrapHistory('pushState');
|
|
275
|
+
wrapHistory('replaceState');
|
|
276
|
+
window.addEventListener('popstate', postPageChange);
|
|
277
|
+
}
|
|
278
|
+
|
|
216
279
|
// --- Theme ---
|
|
217
280
|
// The author owns this document. Unless it opted in to host theming
|
|
218
281
|
// (hostTheme), only viewer-namespaced --pn-* properties may be written to its
|
|
219
282
|
// root, and its class list is never touched.
|
|
220
283
|
window.addEventListener('message', function(e) {
|
|
221
284
|
if (e.source !== parent) return;
|
|
285
|
+
if (LIVE && (!isEditorOrigin(e.origin) || !e.data || e.data.token !== LIVE.token)) return;
|
|
222
286
|
if (!e.data) return;
|
|
223
287
|
if (e.data.type === PREFIX + 'set-vim-help') {
|
|
224
288
|
vimHelpOpen = !!e.data.open;
|
|
225
|
-
|
|
289
|
+
postToParent({
|
|
226
290
|
type: PREFIX + 'vim-help',
|
|
227
291
|
open: vimHelpOpen
|
|
228
|
-
}
|
|
292
|
+
});
|
|
229
293
|
return;
|
|
230
294
|
}
|
|
231
295
|
if (e.data.type !== PREFIX + 'theme') return;
|
|
@@ -246,11 +310,12 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
246
310
|
// --- Resize ---
|
|
247
311
|
var lastHeight = 0;
|
|
248
312
|
function postResize() {
|
|
313
|
+
if (LIVE) return; // live surfaces render full-viewport; the parent ignores height
|
|
249
314
|
if (!document.body) return;
|
|
250
315
|
var h = document.body.scrollHeight;
|
|
251
316
|
if (h !== lastHeight) {
|
|
252
317
|
lastHeight = h;
|
|
253
|
-
|
|
318
|
+
postToParent({ type: PREFIX + 'resize', height: h });
|
|
254
319
|
}
|
|
255
320
|
}
|
|
256
321
|
window.addEventListener('load', postResize);
|
|
@@ -274,7 +339,28 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
274
339
|
var pendingMultiTargets = []; // { key, el, anchor, label, text, box }
|
|
275
340
|
var multiTargetSeq = 0;
|
|
276
341
|
var MAX_MULTI_TARGETS = 16;
|
|
277
|
-
|
|
342
|
+
// Live mode clamps the INPUT METHOD to pinpoint (click = element). Text
|
|
343
|
+
// drag-selection is a separate, always-on channel — see the mouseup handler
|
|
344
|
+
// — so the clamp only decides what a plain click does, never whether text
|
|
345
|
+
// can be selected and commented.
|
|
346
|
+
var currentInputMethod = LIVE ? 'pinpoint' : 'drag'; // 'drag' = text selection, 'pinpoint' = click an element
|
|
347
|
+
// Interact/Annotate mode. While INACTIVE the bridge keeps clicks native: no
|
|
348
|
+
// pinpoint capture, no hover outline, no [data-annotate] click, no
|
|
349
|
+
// committed-highlight click interception — clicks, forms, and SPA
|
|
350
|
+
// navigation reach the page untouched. Text drag-selection commenting stays
|
|
351
|
+
// LIVE in both modes (a real drag opens the comment toolbar even in
|
|
352
|
+
// Interact), committed overlay artifacts stay visible in both modes, and
|
|
353
|
+
// marker buttons keep their own clicks. BOTH surface kinds start ARMED —
|
|
354
|
+
// Esc (or the header pen) drops to Interact.
|
|
355
|
+
var annotateModeActive = true;
|
|
356
|
+
function updatePinpointCursor() {
|
|
357
|
+
if (!document.body) return;
|
|
358
|
+
if (annotateModeActive && currentInputMethod === 'pinpoint') {
|
|
359
|
+
document.body.setAttribute('data-plannotator-pinpoint-cursor', '');
|
|
360
|
+
} else {
|
|
361
|
+
document.body.removeAttribute('data-plannotator-pinpoint-cursor');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
278
364
|
var pinpointHover = null;
|
|
279
365
|
var vimEnabled = false;
|
|
280
366
|
var vimHudEnabled = false;
|
|
@@ -295,10 +381,9 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
295
381
|
// and immediately clear it. This flag suppresses that one trailing clear.
|
|
296
382
|
var skipNextClear = false;
|
|
297
383
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
});
|
|
384
|
+
// Drag-selection commenting is handled by the merged capture-phase mouseup
|
|
385
|
+
// listener below (after the drag-yield state it reads) — it is ALWAYS live:
|
|
386
|
+
// both surfaces, armed or Interact, drag or pinpoint input method.
|
|
302
387
|
|
|
303
388
|
// The page fully controls element text, so everything posted as a selection
|
|
304
389
|
// is bounded here before it crosses the bridge (the parent enforces the same
|
|
@@ -322,7 +407,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
322
407
|
// Trailing clear from a plain-click element annotation — consume it once.
|
|
323
408
|
if (skipNextClear) { skipNextClear = false; return; }
|
|
324
409
|
if (pendingSelection) {
|
|
325
|
-
|
|
410
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
326
411
|
pendingSelection = null;
|
|
327
412
|
pendingRange = null;
|
|
328
413
|
clearMultiTargets();
|
|
@@ -350,7 +435,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
350
435
|
endOffset: range.endOffset
|
|
351
436
|
};
|
|
352
437
|
|
|
353
|
-
|
|
438
|
+
postToParent({
|
|
354
439
|
type: PREFIX + 'selection',
|
|
355
440
|
text: text,
|
|
356
441
|
modeOverride: modeOverride || undefined,
|
|
@@ -359,7 +444,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
359
444
|
targetKey: (extras && extras.targetKey) || undefined,
|
|
360
445
|
targetLabel: (extras && extras.targetLabel) || undefined,
|
|
361
446
|
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
|
|
362
|
-
}
|
|
447
|
+
});
|
|
363
448
|
renderAnnotationOverlay(); // draft selection highlight (overlay-projected)
|
|
364
449
|
return true;
|
|
365
450
|
}
|
|
@@ -382,17 +467,17 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
382
467
|
// Drag selections keep the existing close-on-scroll-out behavior.
|
|
383
468
|
if (pendingPinViaPinpoint || pendingMultiTargets.length > 0) return;
|
|
384
469
|
// Selection scrolled out of view — close the toolbar (matches markdown).
|
|
385
|
-
|
|
470
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
386
471
|
pendingSelection = null;
|
|
387
472
|
pendingRange = null;
|
|
388
473
|
clearPendingPin();
|
|
389
474
|
renderAnnotationOverlay();
|
|
390
475
|
return;
|
|
391
476
|
}
|
|
392
|
-
|
|
477
|
+
postToParent({
|
|
393
478
|
type: PREFIX + 'selection-rect',
|
|
394
479
|
rect: { top: r.top, left: r.left, width: r.width, height: r.height }
|
|
395
|
-
}
|
|
480
|
+
});
|
|
396
481
|
}
|
|
397
482
|
window.addEventListener('scroll', function() {
|
|
398
483
|
if (!pendingSelection) return;
|
|
@@ -402,6 +487,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
402
487
|
// --- Mark Creation ---
|
|
403
488
|
window.addEventListener('message', function(e) {
|
|
404
489
|
if (e.source !== parent) return;
|
|
490
|
+
if (LIVE && (!isEditorOrigin(e.origin) || !e.data || e.data.token !== LIVE.token)) return;
|
|
405
491
|
if (!e.data || !e.data.type) return;
|
|
406
492
|
var type = e.data.type;
|
|
407
493
|
|
|
@@ -483,11 +569,11 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
483
569
|
e.data.anchor,
|
|
484
570
|
e.data.additionalAnchors
|
|
485
571
|
);
|
|
486
|
-
|
|
572
|
+
postToParent({
|
|
487
573
|
type: PREFIX + 'mark-applied',
|
|
488
574
|
id: e.data.id,
|
|
489
575
|
success: found
|
|
490
|
-
}
|
|
576
|
+
});
|
|
491
577
|
}
|
|
492
578
|
|
|
493
579
|
else if (type === PREFIX + 'remove-mark') {
|
|
@@ -499,6 +585,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
499
585
|
annRecords = [];
|
|
500
586
|
annNumbers = null; // stale synced numbers must not leak onto future records
|
|
501
587
|
focusedAnnotationId = null;
|
|
588
|
+
restoreFailedIds.clear();
|
|
502
589
|
renderAnnotationOverlay();
|
|
503
590
|
}
|
|
504
591
|
|
|
@@ -577,18 +664,44 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
577
664
|
}
|
|
578
665
|
|
|
579
666
|
else if (type === PREFIX + 'set-input-method') {
|
|
580
|
-
|
|
667
|
+
// Live mode clamps the input method to pinpoint (what a plain click
|
|
668
|
+
// does); text drag-selection commenting stays live regardless.
|
|
669
|
+
currentInputMethod = (LIVE || e.data.method === 'pinpoint') ? 'pinpoint' : 'drag';
|
|
581
670
|
if (currentInputMethod === 'pinpoint') {
|
|
582
671
|
clearHoverHighlight(); // pinpoint owns clicks; drop the select affordance (and any pending hit test)
|
|
583
|
-
if (document.body) document.body.setAttribute('data-plannotator-pinpoint-cursor', '');
|
|
584
672
|
} else {
|
|
585
|
-
if (document.body) document.body.removeAttribute('data-plannotator-pinpoint-cursor');
|
|
586
673
|
clearPinpointHover();
|
|
587
674
|
}
|
|
675
|
+
updatePinpointCursor(); // cursor affordance is mode-gated: never in Interact
|
|
588
676
|
if (vimEnabled) updateVimUi();
|
|
589
677
|
}
|
|
590
678
|
|
|
679
|
+
else if (type === PREFIX + 'set-annotate-mode') {
|
|
680
|
+
var nextAnnotateActive = e.data.active === true;
|
|
681
|
+
if (nextAnnotateActive !== annotateModeActive) {
|
|
682
|
+
annotateModeActive = nextAnnotateActive;
|
|
683
|
+
if (!annotateModeActive) {
|
|
684
|
+
// Disarm tears down every PENDING affordance; committed markers and
|
|
685
|
+
// highlights stay visible, and marker buttons keep their clicks.
|
|
686
|
+
if (pendingSelection) postToParent({ type: PREFIX + 'selection-clear' });
|
|
687
|
+
pendingSelection = null;
|
|
688
|
+
pendingRange = null;
|
|
689
|
+
skipNextClear = false;
|
|
690
|
+
clearMultiTargets();
|
|
691
|
+
clearPendingPin();
|
|
692
|
+
try { window.getSelection().removeAllRanges(); } catch (ex) {}
|
|
693
|
+
clearPinpointHover();
|
|
694
|
+
clearHoverHighlight();
|
|
695
|
+
renderAnnotationOverlay();
|
|
696
|
+
}
|
|
697
|
+
updatePinpointCursor();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
591
701
|
else if (type === PREFIX + 'set-vim-mode') {
|
|
702
|
+
// Vim is off in live mode: it writes classes onto author elements and
|
|
703
|
+
// captures keys the app needs.
|
|
704
|
+
if (LIVE) return;
|
|
592
705
|
var wasVimEnabled = vimEnabled;
|
|
593
706
|
var wasVimHudEnabled = vimHudEnabled;
|
|
594
707
|
vimEnabled = e.data.enabled === true;
|
|
@@ -1095,6 +1208,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1095
1208
|
document.addEventListener('mousemove', function(e) {
|
|
1096
1209
|
lastPointer = { x: e.clientX, y: e.clientY };
|
|
1097
1210
|
updateDragYield(e);
|
|
1211
|
+
if (!annotateModeActive) return; // Interact mode: no hover affordances
|
|
1098
1212
|
if (currentInputMethod !== 'pinpoint') {
|
|
1099
1213
|
// Click-to-select hover affordance (drag mode owns highlight clicks):
|
|
1100
1214
|
// cheap cached-rect hit test, rAF-throttled, cleared while a draft or
|
|
@@ -1107,6 +1221,9 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1107
1221
|
return;
|
|
1108
1222
|
}
|
|
1109
1223
|
if (vimEnabled && vimPhase !== 'inactive') return;
|
|
1224
|
+
// Mid-drag (text selection in progress) the element hover box is noise:
|
|
1225
|
+
// the drag owns the surface until mouseup resolves it.
|
|
1226
|
+
if (dragYieldActive) { clearPinpointHover(); return; }
|
|
1110
1227
|
// Hit-test at the pointer (e.target only backstops engines without
|
|
1111
1228
|
// elementFromPoint) so the same code path serves the scroll re-hit-test.
|
|
1112
1229
|
updatePinpointHover(e.clientX, e.clientY, e.target);
|
|
@@ -1136,7 +1253,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1136
1253
|
renderAnnotationOverlay();
|
|
1137
1254
|
// Scroll under a stationary pointer moves the committed rects: re-run
|
|
1138
1255
|
// the cached-rect hover test so the affordance tracks reality.
|
|
1139
|
-
if (currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
|
|
1256
|
+
if (annotateModeActive && currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
|
|
1140
1257
|
setHoverHighlight(committedHighlightIdAtCached(lastPointer.x, lastPointer.y));
|
|
1141
1258
|
}
|
|
1142
1259
|
positionMultiTargetBoxes();
|
|
@@ -1144,7 +1261,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1144
1261
|
positionPinpointBox(pendingPinEl);
|
|
1145
1262
|
return;
|
|
1146
1263
|
}
|
|
1147
|
-
if (currentInputMethod !== 'pinpoint') return;
|
|
1264
|
+
if (!annotateModeActive || currentInputMethod !== 'pinpoint') return;
|
|
1148
1265
|
if (vimEnabled && vimPhase !== 'inactive') return;
|
|
1149
1266
|
if (lastPointer) {
|
|
1150
1267
|
updatePinpointHover(lastPointer.x, lastPointer.y, pinpointHover);
|
|
@@ -1258,15 +1375,21 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1258
1375
|
}
|
|
1259
1376
|
}
|
|
1260
1377
|
|
|
1261
|
-
// Drag-selection marker yield: while a text drag is in progress
|
|
1262
|
-
//
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1378
|
+
// Drag-selection marker yield: while a text drag is in progress, placed
|
|
1379
|
+
// markers drop pointer input (the same data-pn-hittest CSS the hit-test
|
|
1380
|
+
// yield uses) so a 25px bubble sitting over the text cannot capture the
|
|
1381
|
+
// selection mid-drag. Armed only by a >4px move with the primary button
|
|
1382
|
+
// held from a non-overlay mousedown, so marker clicks (mousedown ON the
|
|
1383
|
+
// marker) and plain click-to-select (no drag) are untouched; disarmed on
|
|
1384
|
+
// mouseup or when the button is seen released. The same >4px arming is
|
|
1385
|
+
// what tells the mouseup handler below that a REAL drag happened, which
|
|
1386
|
+
// is how drag-selection commenting stays live in pinpoint/armed mode
|
|
1387
|
+
// without a plain pinpoint click re-posting its own selection.
|
|
1268
1388
|
var dragYieldStart = null;
|
|
1269
1389
|
var dragYieldActive = false;
|
|
1390
|
+
// True between a drag's terminating mouseup and the next mousedown: the
|
|
1391
|
+
// click event that follows a completed drag must not pinpoint-annotate.
|
|
1392
|
+
var dragEndedClick = false;
|
|
1270
1393
|
function endDragYield() {
|
|
1271
1394
|
dragYieldStart = null;
|
|
1272
1395
|
if (dragYieldActive) {
|
|
@@ -1291,13 +1414,37 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1291
1414
|
}
|
|
1292
1415
|
}
|
|
1293
1416
|
document.addEventListener('mousedown', function(e) {
|
|
1294
|
-
|
|
1417
|
+
dragEndedClick = false;
|
|
1295
1418
|
if (e.button !== 0) return;
|
|
1296
1419
|
if (isViewerOverlayNode(e.target)) return;
|
|
1297
1420
|
dragYieldStart = { x: e.clientX, y: e.clientY };
|
|
1298
1421
|
}, true);
|
|
1299
1422
|
document.addEventListener('mouseup', function() {
|
|
1423
|
+
// Text drag-selection commenting is ALWAYS live: both surfaces, armed or
|
|
1424
|
+
// Interact. In armed pinpoint a plain click belongs to the pinpoint
|
|
1425
|
+
// handler — only a drag that actually PRODUCED a text selection owns its
|
|
1426
|
+
// trailing click, so annotateElement's own selection is never re-posted
|
|
1427
|
+
// by its trailing mouseup. The >4px drift alone is NOT enough to arm the
|
|
1428
|
+
// click suppression: a drifted click (common on trackpads) that selected
|
|
1429
|
+
// nothing must stay a click, or armed pinpoint would silently swallow it
|
|
1430
|
+
// while the unprevented click leaks through to the page. Browsers clear
|
|
1431
|
+
// a prior selection on the mousedown that starts the drag, so the
|
|
1432
|
+
// selection observed here is the one THIS drag produced. Everywhere else
|
|
1433
|
+
// the classic always-schedule behavior stays: handleSelection only acts
|
|
1434
|
+
// on a real selection, posts the clear that dismisses a stale draft, and
|
|
1435
|
+
// never preventDefaults — a plain click is never swallowed.
|
|
1436
|
+
var dragged = dragYieldActive;
|
|
1300
1437
|
endDragYield();
|
|
1438
|
+
var draggedSelection = false;
|
|
1439
|
+
if (dragged) {
|
|
1440
|
+
try {
|
|
1441
|
+
var s = window.getSelection();
|
|
1442
|
+
draggedSelection = !!(s && !s.isCollapsed && s.rangeCount && (s.toString() || '').trim());
|
|
1443
|
+
} catch (ex) {}
|
|
1444
|
+
}
|
|
1445
|
+
dragEndedClick = draggedSelection;
|
|
1446
|
+
if (annotateModeActive && currentInputMethod === 'pinpoint' && !draggedSelection) return;
|
|
1447
|
+
setTimeout(handleSelection, 10);
|
|
1301
1448
|
}, true);
|
|
1302
1449
|
|
|
1303
1450
|
// One record per committed annotation. Its targets are live projections;
|
|
@@ -1327,9 +1474,47 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1327
1474
|
for (var i = annRecords.length - 1; i >= 0; i--) {
|
|
1328
1475
|
if (annRecords[i].id === id) annRecords.splice(i, 1);
|
|
1329
1476
|
}
|
|
1477
|
+
restoreFailedIds['delete'](id);
|
|
1330
1478
|
if (focusedAnnotationId === id) focusedAnnotationId = null;
|
|
1331
1479
|
}
|
|
1332
1480
|
|
|
1481
|
+
// Fail-closed transparency (host ask): markers for dead targets are
|
|
1482
|
+
// omitted, never guessed — this names WHICH annotations currently have no
|
|
1483
|
+
// live representation on the page (every target dead, or the restore never
|
|
1484
|
+
// resolved anything) so the host can tell the user instead of letting them
|
|
1485
|
+
// silently vanish. Emitted only when the set changes, and only from
|
|
1486
|
+
// complete overlay passes — budget-starved passes reschedule themselves
|
|
1487
|
+
// and would flap the set. restoreFailedIds carries total restore failures,
|
|
1488
|
+
// whose records are removed and therefore invisible to the per-pass scan.
|
|
1489
|
+
var lastUnanchoredKey = '[]';
|
|
1490
|
+
var restoreFailedIds = new Set();
|
|
1491
|
+
function emitUnanchored(deadRecordIds) {
|
|
1492
|
+
var seen = new Set();
|
|
1493
|
+
var combined = [];
|
|
1494
|
+
for (var deadIndex = 0; deadIndex < deadRecordIds.length; deadIndex++) {
|
|
1495
|
+
if (!seen.has(deadRecordIds[deadIndex])) {
|
|
1496
|
+
seen.add(deadRecordIds[deadIndex]);
|
|
1497
|
+
combined.push(deadRecordIds[deadIndex]);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
restoreFailedIds.forEach(function(failedId) {
|
|
1501
|
+
if (!seen.has(failedId)) {
|
|
1502
|
+
seen.add(failedId);
|
|
1503
|
+
combined.push(failedId);
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
combined.sort();
|
|
1507
|
+
if (combined.length > 512) combined = combined.slice(0, 512);
|
|
1508
|
+
var key = JSON.stringify(combined);
|
|
1509
|
+
if (key === lastUnanchoredKey) return;
|
|
1510
|
+
lastUnanchoredKey = key;
|
|
1511
|
+
// postToParent, not a raw '*' post: live sessions stamp the session
|
|
1512
|
+
// token and post only to the listed editor origins, and the parent
|
|
1513
|
+
// drops untokened live messages — a raw post would silently disable
|
|
1514
|
+
// unanchored reporting exactly where restores fail most (live pages).
|
|
1515
|
+
postToParent({ type: PREFIX + 'unanchored', ids: combined });
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1333
1518
|
function validNormalizedPoint(p) {
|
|
1334
1519
|
if (!p || typeof p.x !== 'number' || typeof p.y !== 'number') return null;
|
|
1335
1520
|
if (!isFinite(p.x) || !isFinite(p.y)) return null;
|
|
@@ -1467,7 +1652,50 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1467
1652
|
}
|
|
1468
1653
|
}
|
|
1469
1654
|
}
|
|
1470
|
-
if (!record.targets.length)
|
|
1655
|
+
if (!record.targets.length && LIVE) {
|
|
1656
|
+
// Live pages render late (lazy routes, data-dependent trees): a
|
|
1657
|
+
// restore that resolves nothing keeps its record, seeded with
|
|
1658
|
+
// unresolved placeholder targets built from the durable params, so
|
|
1659
|
+
// the mutation-driven reconcile re-acquires them when the elements
|
|
1660
|
+
// appear. Srcdoc documents are static (nothing would ever
|
|
1661
|
+
// re-resolve), so the record is dropped below exactly as before.
|
|
1662
|
+
if (anchor) {
|
|
1663
|
+
record.targets.push({
|
|
1664
|
+
kind: 'element',
|
|
1665
|
+
element: null,
|
|
1666
|
+
anchor: anchor,
|
|
1667
|
+
point: normalizedPointOf(anchor, null)
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
if (originalText) {
|
|
1671
|
+
record.targets.push({
|
|
1672
|
+
kind: 'range',
|
|
1673
|
+
range: null,
|
|
1674
|
+
text: originalText,
|
|
1675
|
+
markerless: !!anchor
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
if (additionalAnchors && additionalAnchors.length) {
|
|
1679
|
+
var lateCount = Math.min(additionalAnchors.length, MAX_MULTI_TARGETS);
|
|
1680
|
+
for (var lateIndex = 0; lateIndex < lateCount; lateIndex++) {
|
|
1681
|
+
if (!additionalAnchors[lateIndex]) continue;
|
|
1682
|
+
record.targets.push({
|
|
1683
|
+
kind: 'element',
|
|
1684
|
+
element: null,
|
|
1685
|
+
anchor: additionalAnchors[lateIndex],
|
|
1686
|
+
point: normalizedPointOf(additionalAnchors[lateIndex], null)
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (!record.targets.length) {
|
|
1692
|
+
// The record is removed (nothing to retry), so the per-pass dead scan
|
|
1693
|
+
// cannot see this id: track it separately for the unanchored report.
|
|
1694
|
+
// Live sessions only reach here when the durable params seeded no
|
|
1695
|
+
// placeholder targets at all (nothing will ever re-resolve).
|
|
1696
|
+
removeAnnRecord(id);
|
|
1697
|
+
restoreFailedIds.add(id);
|
|
1698
|
+
}
|
|
1471
1699
|
// rAF-coalesced render (B3): restoring N annotations posts N
|
|
1472
1700
|
// find-and-mark messages, and a synchronous render here made a batch
|
|
1473
1701
|
// restore O(N^2) full overlay passes. The searches above stay
|
|
@@ -2027,7 +2255,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2027
2255
|
btn.addEventListener('click', function(clickEvent) {
|
|
2028
2256
|
clickEvent.preventDefault();
|
|
2029
2257
|
clickEvent.stopPropagation();
|
|
2030
|
-
|
|
2258
|
+
postToParent({ type: PREFIX + 'mark-click', id: annId });
|
|
2031
2259
|
});
|
|
2032
2260
|
overlayNodes.add(btn);
|
|
2033
2261
|
return btn;
|
|
@@ -2094,10 +2322,17 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2094
2322
|
|
|
2095
2323
|
function renderAnnotationOverlay() {
|
|
2096
2324
|
var hasDraftRange = !!(pendingSelection && pendingRange);
|
|
2097
|
-
if (!annRecords.length && !hasDraftRange && !overlayHostEl)
|
|
2325
|
+
if (!annRecords.length && !hasDraftRange && !overlayHostEl) {
|
|
2326
|
+
// Nothing to project, but total restore failures must still report:
|
|
2327
|
+
// a session whose only annotations failed to restore never builds the
|
|
2328
|
+
// overlay host, and silence here would hide exactly that case.
|
|
2329
|
+
emitUnanchored([]);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2098
2332
|
ensureOverlayHost();
|
|
2099
2333
|
beginDeadSearchPass();
|
|
2100
2334
|
queuedHighlights.length = 0;
|
|
2335
|
+
var unanchoredThisPass = [];
|
|
2101
2336
|
var markers = [];
|
|
2102
2337
|
// Fallback numbering by first-seen registration order — used only until
|
|
2103
2338
|
// the parent's ordered sync arrives. Numbering NEVER derives from target
|
|
@@ -2111,6 +2346,20 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2111
2346
|
for (var recordIndex = 0; recordIndex < annRecords.length; recordIndex++) {
|
|
2112
2347
|
var record = annRecords[recordIndex];
|
|
2113
2348
|
refreshRecordTargets(record);
|
|
2349
|
+
// Live means connected/findable — clipped, offscreen, or style-hidden
|
|
2350
|
+
// targets are still anchored (their content exists; it just isn't
|
|
2351
|
+
// currently visible) and must not report as unanchored.
|
|
2352
|
+
var recordAnchored = false;
|
|
2353
|
+
for (var liveIndex = 0; liveIndex < record.targets.length; liveIndex++) {
|
|
2354
|
+
var liveTarget = record.targets[liveIndex];
|
|
2355
|
+
if (liveTarget.kind === 'element'
|
|
2356
|
+
? (liveTarget.element && liveTarget.element.isConnected)
|
|
2357
|
+
: rangeAlive(liveTarget.range)) {
|
|
2358
|
+
recordAnchored = true;
|
|
2359
|
+
break;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
if (!recordAnchored) unanchoredThisPass.push(record.id);
|
|
2114
2363
|
var number = annNumbers && annNumbers.has(record.id)
|
|
2115
2364
|
? annNumbers.get(record.id)
|
|
2116
2365
|
: fallbackNumbers.get(record.id);
|
|
@@ -2219,6 +2468,9 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2219
2468
|
// record.
|
|
2220
2469
|
flushQueuedHighlights();
|
|
2221
2470
|
placeMarkers(markers);
|
|
2471
|
+
// Budget-starved passes reschedule below and may still revive targets,
|
|
2472
|
+
// so only a complete pass may update the unanchored report.
|
|
2473
|
+
if (!deadSearchSkipped) emitUnanchored(unanchoredThisPass);
|
|
2222
2474
|
// Eligible dead-target searches skipped for budget get a follow-up pass;
|
|
2223
2475
|
// the loop terminates once every eligible target has been attempted at
|
|
2224
2476
|
// the current generation (its failedGeneration then blocks it).
|
|
@@ -2544,7 +2796,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2544
2796
|
clearPendingPin();
|
|
2545
2797
|
try { window.getSelection().removeAllRanges(); } catch (ex) {}
|
|
2546
2798
|
renderAnnotationOverlay();
|
|
2547
|
-
if (echo)
|
|
2799
|
+
if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
|
|
2548
2800
|
return;
|
|
2549
2801
|
}
|
|
2550
2802
|
var next = pendingMultiTargets.shift();
|
|
@@ -2564,14 +2816,14 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2564
2816
|
mainBox.classList.remove('pn-pin-enter');
|
|
2565
2817
|
if (pendingPinEl && pendingPinEl.isConnected) positionPinpointBox(pendingPinEl);
|
|
2566
2818
|
renderAnnotationOverlay();
|
|
2567
|
-
if (echo)
|
|
2819
|
+
if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
|
|
2568
2820
|
return;
|
|
2569
2821
|
}
|
|
2570
2822
|
for (var i = 0; i < pendingMultiTargets.length; i++) {
|
|
2571
2823
|
if (pendingMultiTargets[i].key === key) {
|
|
2572
2824
|
destroyMultiTargetBox(pendingMultiTargets[i].box);
|
|
2573
2825
|
pendingMultiTargets.splice(i, 1);
|
|
2574
|
-
if (echo)
|
|
2826
|
+
if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
|
|
2575
2827
|
return;
|
|
2576
2828
|
}
|
|
2577
2829
|
}
|
|
@@ -2614,13 +2866,13 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2614
2866
|
var key = makeTargetKey();
|
|
2615
2867
|
var box = createMultiTargetBox(el);
|
|
2616
2868
|
pendingMultiTargets.push({ key: key, el: el, anchor: anchor, label: label, text: text, point: point, box: box });
|
|
2617
|
-
|
|
2869
|
+
postToParent({
|
|
2618
2870
|
type: PREFIX + 'multi-target-added',
|
|
2619
2871
|
key: key,
|
|
2620
2872
|
label: label,
|
|
2621
2873
|
text: text,
|
|
2622
2874
|
anchor: anchor || undefined
|
|
2623
|
-
}
|
|
2875
|
+
});
|
|
2624
2876
|
}
|
|
2625
2877
|
|
|
2626
2878
|
/** Chip hover in the composer: flash the corresponding pinned outline. */
|
|
@@ -2684,12 +2936,12 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2684
2936
|
pointerRelayRaf = requestAnimationFrame(function() {
|
|
2685
2937
|
pointerRelayRaf = 0;
|
|
2686
2938
|
if (!pendingPinEl || !pointerRelayPos) return;
|
|
2687
|
-
|
|
2939
|
+
postToParent({
|
|
2688
2940
|
type: PREFIX + 'pointer',
|
|
2689
2941
|
x: pointerRelayPos.x,
|
|
2690
2942
|
y: pointerRelayPos.y,
|
|
2691
2943
|
shift: pointerRelayPos.shift
|
|
2692
|
-
}
|
|
2944
|
+
});
|
|
2693
2945
|
});
|
|
2694
2946
|
}
|
|
2695
2947
|
|
|
@@ -2759,22 +3011,31 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2759
3011
|
pendingSelection = { element: true };
|
|
2760
3012
|
pendingRange = null;
|
|
2761
3013
|
skipNextClear = true; // don't let this click's mouseup clear the toolbar we just opened
|
|
2762
|
-
|
|
3014
|
+
postToParent({ type: PREFIX + 'selection', text: elText,
|
|
2763
3015
|
modeOverride: modeOverride || undefined,
|
|
2764
3016
|
anchor: pendingPinAnchor || undefined,
|
|
2765
3017
|
pinpoint: !!viaPinpoint || undefined,
|
|
2766
3018
|
targetKey: pendingPinKey || undefined,
|
|
2767
3019
|
targetLabel: pendingPinLabel || undefined,
|
|
2768
|
-
rect: { top: r.top, left: r.left, width: r.width, height: r.height } }
|
|
3020
|
+
rect: { top: r.top, left: r.left, width: r.width, height: r.height } });
|
|
2769
3021
|
return true;
|
|
2770
3022
|
}
|
|
2771
3023
|
|
|
2772
3024
|
document.addEventListener('click', function(e) {
|
|
2773
|
-
if (currentInputMethod !== 'pinpoint') return;
|
|
3025
|
+
if (!annotateModeActive || currentInputMethod !== 'pinpoint') return;
|
|
2774
3026
|
// Real placed markers (and any other viewer overlay) own their clicks —
|
|
2775
3027
|
// checked by IDENTITY, not selector, so a page element spoofing
|
|
2776
3028
|
// [data-plannotator-marker] stays an ordinary annotatable target.
|
|
2777
3029
|
if (isViewerOverlayNode(e.target)) return;
|
|
3030
|
+
// A drag that ended in this click owns the surface: the drag-selection
|
|
3031
|
+
// pass is about to post the selected text, and pinpoint-annotating the
|
|
3032
|
+
// element under the pointer would clobber it (annotateElement rewrites
|
|
3033
|
+
// the selection). Armed at mouseup only when the drag actually produced
|
|
3034
|
+
// a selection — a drifted click arms nothing and pins normally below —
|
|
3035
|
+
// and mousedown clears a prior pin's leftover selection, so stale
|
|
3036
|
+
// selection state never blocks the next plain re-pin click. One-shot:
|
|
3037
|
+
// only the drag's own trailing click is suppressed.
|
|
3038
|
+
if (dragEndedClick) { dragEndedClick = false; return; }
|
|
2778
3039
|
// Shift-click while an ARMED pinpoint draft is open: toggle the element
|
|
2779
3040
|
// in/out of the SAME draft comment instead of replacing the selection.
|
|
2780
3041
|
// Unarmed drafts (modes the parent does not mirror, e.g. quickLabel)
|
|
@@ -2801,24 +3062,51 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2801
3062
|
annotateElement(el, undefined, true, { x: e.clientX, y: e.clientY });
|
|
2802
3063
|
}, true);
|
|
2803
3064
|
|
|
2804
|
-
// Escape
|
|
2805
|
-
//
|
|
3065
|
+
// Escape ladder (outside vim, which has its own): a pending draft closes
|
|
3066
|
+
// first, then the hover outline clears, then Esc EXITS Annotate back to
|
|
3067
|
+
// Interact — the parent owns the mode, so the final rung only posts
|
|
3068
|
+
// annotate-exit and waits for set-annotate-mode to come back down.
|
|
3069
|
+
function closePendingDraft() {
|
|
3070
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
3071
|
+
pendingSelection = null;
|
|
3072
|
+
pendingRange = null;
|
|
3073
|
+
skipNextClear = false;
|
|
3074
|
+
clearMultiTargets();
|
|
3075
|
+
clearPendingPin();
|
|
3076
|
+
window.getSelection().removeAllRanges();
|
|
3077
|
+
renderAnnotationOverlay();
|
|
3078
|
+
}
|
|
2806
3079
|
document.addEventListener('keydown', function(e) {
|
|
2807
3080
|
if (e.key !== 'Escape' || vimEnabled) return;
|
|
3081
|
+
if (!annotateModeActive) {
|
|
3082
|
+
// Interact mode: Esc belongs to the page — except an open drag-comment
|
|
3083
|
+
// draft (drag-selection stays live in Interact) still closes first.
|
|
3084
|
+
if (pendingSelection) closePendingDraft();
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
2808
3087
|
if (pendingSelection) {
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
} else if (currentInputMethod === 'pinpoint') {
|
|
2818
|
-
clearPinpointHover();
|
|
3088
|
+
closePendingDraft();
|
|
3089
|
+
} else {
|
|
3090
|
+
// Hover-clear is NOT a rung: the hover outline is a pointer
|
|
3091
|
+
// affordance, not a state the user perceives as a step, so clearing
|
|
3092
|
+
// it and exiting to Interact happen on the SAME press. Only an open
|
|
3093
|
+
// draft earns its own press.
|
|
3094
|
+
if (currentInputMethod === 'pinpoint' && pinpointHover) clearPinpointHover();
|
|
3095
|
+
postToParent({ type: PREFIX + 'annotate-exit' });
|
|
2819
3096
|
}
|
|
2820
3097
|
});
|
|
2821
3098
|
|
|
3099
|
+
// Mod+Shift+A toggles Interact/Annotate from inside the iframe (the parent
|
|
3100
|
+
// registers the same chord, but focus usually lives in here on live apps).
|
|
3101
|
+
// Capture phase so the page cannot swallow the reserved chord; the parent
|
|
3102
|
+
// answers with set-annotate-mode.
|
|
3103
|
+
document.addEventListener('keydown', function(e) {
|
|
3104
|
+
if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.altKey) return;
|
|
3105
|
+
if (e.key !== 'a' && e.key !== 'A') return;
|
|
3106
|
+
e.preventDefault();
|
|
3107
|
+
postToParent({ type: PREFIX + 'annotate-toggle' });
|
|
3108
|
+
}, true);
|
|
3109
|
+
|
|
2822
3110
|
// Author opt-in: a plain click on any element tagged [data-annotate] pops the
|
|
2823
3111
|
// toolbar — no pinpoint mode. Lets an HTML doc (e.g. a flow graph) wire its own
|
|
2824
3112
|
// nodes to Plannotator's toolbar. Bubble phase so the page's own click handlers
|
|
@@ -2826,6 +3114,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2826
3114
|
// a committed highlight selects the annotation instead (the pre-overlay
|
|
2827
3115
|
// handler deferred to '.annotation-highlight' the same way).
|
|
2828
3116
|
document.addEventListener('click', function(e) {
|
|
3117
|
+
if (!annotateModeActive) return; // Interact mode: [data-annotate] stays a page element
|
|
2829
3118
|
if (currentInputMethod === 'pinpoint') return; // pinpoint handler covers this
|
|
2830
3119
|
if (isViewerOverlayNode(e.target)) return; // placed markers own their clicks
|
|
2831
3120
|
var t = e.target && e.target.closest && e.target.closest('[data-annotate]');
|
|
@@ -2869,6 +3158,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2869
3158
|
}
|
|
2870
3159
|
|
|
2871
3160
|
document.addEventListener('click', function(e) {
|
|
3161
|
+
// Interact mode: highlight rects are pointer-transparent projections, so a
|
|
3162
|
+
// page click landing on one goes to the page. Markers (real buttons) stay
|
|
3163
|
+
// the affordance for opening a committed comment in Interact.
|
|
3164
|
+
if (!annotateModeActive) return;
|
|
2872
3165
|
if (e.shiftKey) return; // shift belongs to multi-select
|
|
2873
3166
|
if (isViewerOverlayNode(e.target)) return; // markers own their clicks
|
|
2874
3167
|
if (pendingPinEl) return; // an open pinpoint draft owns the surface
|
|
@@ -2877,7 +3170,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2877
3170
|
var hitId = committedHighlightAt(e.clientX, e.clientY);
|
|
2878
3171
|
if (!hitId) return;
|
|
2879
3172
|
e.stopPropagation();
|
|
2880
|
-
|
|
3173
|
+
postToParent({ type: PREFIX + 'mark-click', id: hitId });
|
|
2881
3174
|
});
|
|
2882
3175
|
|
|
2883
3176
|
// --- Optional Vim navigation ---
|
|
@@ -3591,10 +3884,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3591
3884
|
if (!vimEnabled) return;
|
|
3592
3885
|
if (vimHudEnabled && vimLastPostedPhase !== vimPhase) {
|
|
3593
3886
|
vimLastPostedPhase = vimPhase;
|
|
3594
|
-
|
|
3887
|
+
postToParent({
|
|
3595
3888
|
type: PREFIX + 'vim-state',
|
|
3596
3889
|
phase: vimPhase
|
|
3597
|
-
}
|
|
3890
|
+
});
|
|
3598
3891
|
}
|
|
3599
3892
|
var badge = document.querySelector('[data-plannotator-vim-badge]');
|
|
3600
3893
|
if (!vimHudEnabled && !badge) badge = getVimBadgeEl();
|
|
@@ -3656,10 +3949,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3656
3949
|
|
|
3657
3950
|
function toggleVimHelp() {
|
|
3658
3951
|
vimHelpOpen = !vimHelpOpen;
|
|
3659
|
-
|
|
3952
|
+
postToParent({
|
|
3660
3953
|
type: PREFIX + 'vim-help',
|
|
3661
3954
|
open: vimHelpOpen
|
|
3662
|
-
}
|
|
3955
|
+
});
|
|
3663
3956
|
}
|
|
3664
3957
|
|
|
3665
3958
|
function clearVimUi() {
|
|
@@ -3679,10 +3972,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3679
3972
|
|
|
3680
3973
|
function copyVimText(text) {
|
|
3681
3974
|
if (!text) return;
|
|
3682
|
-
|
|
3975
|
+
postToParent({
|
|
3683
3976
|
type: PREFIX + 'vim-copy',
|
|
3684
3977
|
text: text
|
|
3685
|
-
}
|
|
3978
|
+
});
|
|
3686
3979
|
}
|
|
3687
3980
|
|
|
3688
3981
|
function vimActionMode(key) {
|
|
@@ -3744,7 +4037,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3744
4037
|
handled = true;
|
|
3745
4038
|
} else if (key === 'Escape') {
|
|
3746
4039
|
if (pendingSelection) {
|
|
3747
|
-
|
|
4040
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
3748
4041
|
pendingSelection = null;
|
|
3749
4042
|
pendingRange = null;
|
|
3750
4043
|
restoreVimSemanticTarget();
|
|
@@ -3961,12 +4254,12 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3961
4254
|
vimLastActionId = vimActionId;
|
|
3962
4255
|
vimLastActionContext = vimCommandContext;
|
|
3963
4256
|
updateVimReticle();
|
|
3964
|
-
|
|
4257
|
+
postToParent({
|
|
3965
4258
|
type: PREFIX + 'vim-command',
|
|
3966
4259
|
actionId: vimActionId,
|
|
3967
4260
|
key: hudKey,
|
|
3968
4261
|
context: vimCommandContext
|
|
3969
|
-
}
|
|
4262
|
+
});
|
|
3970
4263
|
}
|
|
3971
4264
|
e.preventDefault();
|
|
3972
4265
|
e.stopImmediatePropagation();
|
|
@@ -4000,7 +4293,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4000
4293
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
4001
4294
|
if (!e.key || e.key.length !== 1) return; // single printable char only
|
|
4002
4295
|
e.preventDefault();
|
|
4003
|
-
|
|
4296
|
+
postToParent({ type: PREFIX + 'keytype', key: e.key });
|
|
4004
4297
|
// Hand keyboard focus back to the parent window so the comment textarea can
|
|
4005
4298
|
// take it. Blurring the <iframe> from the parent isn't enough — the inner
|
|
4006
4299
|
// document keeps focus — so the iframe must relinquish it. parent.focus() is
|
|
@@ -4241,7 +4534,13 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4241
4534
|
}).observe(document.body);
|
|
4242
4535
|
}
|
|
4243
4536
|
watchPageMutations();
|
|
4244
|
-
|
|
4537
|
+
// Armed is the default on both surfaces, and live sessions default to
|
|
4538
|
+
// pinpoint: show the cursor affordance immediately instead of waiting for
|
|
4539
|
+
// the parent's first set-input-method/set-annotate-mode round trip.
|
|
4540
|
+
updatePinpointCursor();
|
|
4541
|
+
var readyMsg = { type: PREFIX + 'ready' };
|
|
4542
|
+
if (LIVE) readyMsg.pageUrl = currentPageUrl();
|
|
4543
|
+
postToParent(readyMsg);
|
|
4245
4544
|
}
|
|
4246
4545
|
if (document.readyState === 'loading') {
|
|
4247
4546
|
document.addEventListener('DOMContentLoaded', onReady);
|
|
@@ -4269,3 +4568,22 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4269
4568
|
}
|
|
4270
4569
|
};
|
|
4271
4570
|
})();`;
|
|
4571
|
+
|
|
4572
|
+
/**
|
|
4573
|
+
* Live-mode bootstrap, prepended to BRIDGE_SCRIPT by the annotate server when
|
|
4574
|
+
* composing the proxy-served bridge body. Reads the JSON config prelude
|
|
4575
|
+
* (window.__plannotatorLiveConfig) and installs the annotation CSS that srcdoc
|
|
4576
|
+
* mode splices as a <style> tag. Runs before the bridge IIFE and before its
|
|
4577
|
+
* MutationObserver exists, so this write never feeds the reconcile loop.
|
|
4578
|
+
* Same escaping rules as BRIDGE_SCRIPT: a dependency-free string constant.
|
|
4579
|
+
*/
|
|
4580
|
+
export const LIVE_BRIDGE_BOOTSTRAP = `(function() {
|
|
4581
|
+
var config = window.__plannotatorLiveConfig;
|
|
4582
|
+
if (!config || typeof config.css !== 'string') return;
|
|
4583
|
+
try {
|
|
4584
|
+
var style = document.createElement('style');
|
|
4585
|
+
style.setAttribute('data-plannotator-live-css', '');
|
|
4586
|
+
style.appendChild(document.createTextNode(config.css));
|
|
4587
|
+
(document.head || document.documentElement).appendChild(style);
|
|
4588
|
+
} catch (ex) {}
|
|
4589
|
+
})();`;
|