@plannotator/ui 0.30.0 → 0.32.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 +46 -1
- package/components/ActionMenu.tsx +6 -1
- package/components/AgentsTab.tsx +8 -9
- package/components/AnalysisLayerToggle.tsx +48 -0
- package/components/AnnotationPanel.tsx +158 -36
- package/components/AnnotationToolbar.tsx +50 -30
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/CommentPopover.tsx +238 -46
- package/components/ConfirmDialog.tsx +42 -28
- package/components/GraphvizBlock.tsx +86 -7
- package/components/HtmlSurfaceControls.tsx +170 -0
- package/components/InlineMarkdown.tsx +25 -4
- 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/MermaidBlock.tsx +60 -26
- 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 +175 -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/blocks/MathBlock.tsx +26 -14
- package/components/core/button.tsx +14 -6
- package/components/html-viewer/HtmlViewer.tsx +320 -41
- package/components/html-viewer/bridge-script.ts +360 -72
- package/components/html-viewer/composerYield.ts +1 -51
- package/components/html-viewer/hostThreads.ts +37 -0
- package/components/html-viewer/index.ts +9 -0
- package/components/html-viewer/unanchored.ts +47 -0
- package/components/html-viewer/useHtmlAnnotation.ts +240 -61
- 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/configure.ts +32 -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/useHtmlRefresh.ts +149 -0
- package/hooks/useIsMobile.ts +37 -0
- package/hooks/useLinkedDoc.ts +7 -0
- package/hooks/useMathRenderer.ts +30 -0
- package/hooks/useScrollViewport.ts +74 -0
- package/hooks/useSharing.ts +31 -5
- package/hooks/useViewportEnvironment.ts +350 -0
- package/package.json +5 -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 +35 -0
- package/utils/annotateAgentTerminal.ts +36 -5
- package/utils/blockTargeting.ts +6 -3
- package/utils/composerYield.ts +45 -0
- package/utils/generateIdentity.ts +64 -14
- package/utils/htmlChrome.ts +20 -16
- package/utils/identity-tater.ts +36 -0
- package/utils/lookAndFeelAnnouncement.ts +12 -8
- package/utils/markdownExtensions.ts +57 -0
- package/utils/math-eager.ts +25 -0
- package/utils/math.ts +146 -0
- package/utils/mermaid-eager.ts +28 -0
- package/utils/mermaid.ts +132 -0
- package/utils/parser.ts +75 -2
- package/utils/quickLabels.ts +13 -0
- package/utils/vimNavigation.ts +4 -1
- package/utils/vimScroll.ts +9 -4
- package/utils/wideMode.ts +20 -0
- package/webmcp/activity.ts +46 -0
- package/webmcp/changes.ts +227 -0
- package/webmcp/index.ts +72 -0
- package/webmcp/modelContext.ts +103 -0
- package/webmcp/nudges.ts +174 -0
- package/webmcp/policy.ts +50 -0
- package/webmcp/preference.ts +50 -0
- package/webmcp/schema.ts +81 -0
- package/webmcp/toolset.ts +337 -0
- package/webmcp/useToolset.ts +74 -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,39 @@ 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
|
+
// Per-draft cap on additional targets: the parent may lower it on
|
|
343
|
+
// arm-multi-select ({ max }) to its product cap so the toggle stops where
|
|
344
|
+
// the saved annotation would. Never above MAX_MULTI_TARGETS; reset with
|
|
345
|
+
// the arm on every draft.
|
|
346
|
+
var multiSelectMax = MAX_MULTI_TARGETS;
|
|
347
|
+
function clampMultiSelectMax(value) {
|
|
348
|
+
if (typeof value !== 'number' || !isFinite(value)) return MAX_MULTI_TARGETS;
|
|
349
|
+
var whole = Math.floor(value);
|
|
350
|
+
if (whole < 0) return 0;
|
|
351
|
+
return whole > MAX_MULTI_TARGETS ? MAX_MULTI_TARGETS : whole;
|
|
352
|
+
}
|
|
353
|
+
// Live mode clamps the INPUT METHOD to pinpoint (click = element). Text
|
|
354
|
+
// drag-selection is a separate, always-on channel — see the mouseup handler
|
|
355
|
+
// — so the clamp only decides what a plain click does, never whether text
|
|
356
|
+
// can be selected and commented.
|
|
357
|
+
var currentInputMethod = LIVE ? 'pinpoint' : 'drag'; // 'drag' = text selection, 'pinpoint' = click an element
|
|
358
|
+
// Interact/Annotate mode. While INACTIVE the bridge keeps clicks native: no
|
|
359
|
+
// pinpoint capture, no hover outline, no [data-annotate] click, no
|
|
360
|
+
// committed-highlight click interception — clicks, forms, and SPA
|
|
361
|
+
// navigation reach the page untouched. Text drag-selection commenting stays
|
|
362
|
+
// LIVE in both modes (a real drag opens the comment toolbar even in
|
|
363
|
+
// Interact), committed overlay artifacts stay visible in both modes, and
|
|
364
|
+
// marker buttons keep their own clicks. BOTH surface kinds start ARMED —
|
|
365
|
+
// Esc (or the header pen) drops to Interact.
|
|
366
|
+
var annotateModeActive = true;
|
|
367
|
+
function updatePinpointCursor() {
|
|
368
|
+
if (!document.body) return;
|
|
369
|
+
if (annotateModeActive && currentInputMethod === 'pinpoint') {
|
|
370
|
+
document.body.setAttribute('data-plannotator-pinpoint-cursor', '');
|
|
371
|
+
} else {
|
|
372
|
+
document.body.removeAttribute('data-plannotator-pinpoint-cursor');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
278
375
|
var pinpointHover = null;
|
|
279
376
|
var vimEnabled = false;
|
|
280
377
|
var vimHudEnabled = false;
|
|
@@ -295,10 +392,9 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
295
392
|
// and immediately clear it. This flag suppresses that one trailing clear.
|
|
296
393
|
var skipNextClear = false;
|
|
297
394
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
});
|
|
395
|
+
// Drag-selection commenting is handled by the merged capture-phase mouseup
|
|
396
|
+
// listener below (after the drag-yield state it reads) — it is ALWAYS live:
|
|
397
|
+
// both surfaces, armed or Interact, drag or pinpoint input method.
|
|
302
398
|
|
|
303
399
|
// The page fully controls element text, so everything posted as a selection
|
|
304
400
|
// is bounded here before it crosses the bridge (the parent enforces the same
|
|
@@ -322,7 +418,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
322
418
|
// Trailing clear from a plain-click element annotation — consume it once.
|
|
323
419
|
if (skipNextClear) { skipNextClear = false; return; }
|
|
324
420
|
if (pendingSelection) {
|
|
325
|
-
|
|
421
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
326
422
|
pendingSelection = null;
|
|
327
423
|
pendingRange = null;
|
|
328
424
|
clearMultiTargets();
|
|
@@ -350,7 +446,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
350
446
|
endOffset: range.endOffset
|
|
351
447
|
};
|
|
352
448
|
|
|
353
|
-
|
|
449
|
+
postToParent({
|
|
354
450
|
type: PREFIX + 'selection',
|
|
355
451
|
text: text,
|
|
356
452
|
modeOverride: modeOverride || undefined,
|
|
@@ -359,7 +455,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
359
455
|
targetKey: (extras && extras.targetKey) || undefined,
|
|
360
456
|
targetLabel: (extras && extras.targetLabel) || undefined,
|
|
361
457
|
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
|
|
362
|
-
}
|
|
458
|
+
});
|
|
363
459
|
renderAnnotationOverlay(); // draft selection highlight (overlay-projected)
|
|
364
460
|
return true;
|
|
365
461
|
}
|
|
@@ -382,17 +478,17 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
382
478
|
// Drag selections keep the existing close-on-scroll-out behavior.
|
|
383
479
|
if (pendingPinViaPinpoint || pendingMultiTargets.length > 0) return;
|
|
384
480
|
// Selection scrolled out of view — close the toolbar (matches markdown).
|
|
385
|
-
|
|
481
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
386
482
|
pendingSelection = null;
|
|
387
483
|
pendingRange = null;
|
|
388
484
|
clearPendingPin();
|
|
389
485
|
renderAnnotationOverlay();
|
|
390
486
|
return;
|
|
391
487
|
}
|
|
392
|
-
|
|
488
|
+
postToParent({
|
|
393
489
|
type: PREFIX + 'selection-rect',
|
|
394
490
|
rect: { top: r.top, left: r.left, width: r.width, height: r.height }
|
|
395
|
-
}
|
|
491
|
+
});
|
|
396
492
|
}
|
|
397
493
|
window.addEventListener('scroll', function() {
|
|
398
494
|
if (!pendingSelection) return;
|
|
@@ -402,6 +498,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
402
498
|
// --- Mark Creation ---
|
|
403
499
|
window.addEventListener('message', function(e) {
|
|
404
500
|
if (e.source !== parent) return;
|
|
501
|
+
if (LIVE && (!isEditorOrigin(e.origin) || !e.data || e.data.token !== LIVE.token)) return;
|
|
405
502
|
if (!e.data || !e.data.type) return;
|
|
406
503
|
var type = e.data.type;
|
|
407
504
|
|
|
@@ -483,11 +580,11 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
483
580
|
e.data.anchor,
|
|
484
581
|
e.data.additionalAnchors
|
|
485
582
|
);
|
|
486
|
-
|
|
583
|
+
postToParent({
|
|
487
584
|
type: PREFIX + 'mark-applied',
|
|
488
585
|
id: e.data.id,
|
|
489
586
|
success: found
|
|
490
|
-
}
|
|
587
|
+
});
|
|
491
588
|
}
|
|
492
589
|
|
|
493
590
|
else if (type === PREFIX + 'remove-mark') {
|
|
@@ -551,6 +648,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
551
648
|
&& e.data.key === pendingPinKey
|
|
552
649
|
) {
|
|
553
650
|
multiSelectArmed = true;
|
|
651
|
+
// Optional product cap for THIS draft; absent keeps the bridge's own.
|
|
652
|
+
multiSelectMax = e.data.max === undefined
|
|
653
|
+
? MAX_MULTI_TARGETS
|
|
654
|
+
: clampMultiSelectMax(e.data.max);
|
|
554
655
|
}
|
|
555
656
|
}
|
|
556
657
|
|
|
@@ -569,27 +670,64 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
569
670
|
// Selecting an annotation scrolls its first resolved target into view
|
|
570
671
|
// and flashes the overlay focus highlight over EVERY rect of EVERY
|
|
571
672
|
// target — never a class write on page elements, and never only the
|
|
572
|
-
// first fragment of a multi-paragraph selection.
|
|
573
|
-
|
|
673
|
+
// first fragment of a multi-paragraph selection. The optional
|
|
674
|
+
// behavior lets the parent pass its reduced-motion preference across
|
|
675
|
+
// the boundary; absent means smooth, as before.
|
|
676
|
+
scrollToAnnotation(e.data.id, e.data.behavior === 'auto' ? 'auto' : 'smooth');
|
|
574
677
|
}
|
|
575
678
|
|
|
576
679
|
else if (type === PREFIX + 'focus-mark') {
|
|
577
680
|
focusAnnotationRecord(typeof e.data.id === 'string' ? e.data.id : null, false);
|
|
578
681
|
}
|
|
579
682
|
|
|
683
|
+
else if (type === PREFIX + 'report-unanchored') {
|
|
684
|
+
// The parent posted its restore batch and wants the complete set once
|
|
685
|
+
// the next complete overlay pass has run, even if the set is unchanged
|
|
686
|
+
// (empty included). Messages are processed in order, so the pass this
|
|
687
|
+
// schedules sees every find-and-mark posted before this request.
|
|
688
|
+
unanchoredReportRequested = true;
|
|
689
|
+
schedulePinpointReconcile();
|
|
690
|
+
}
|
|
691
|
+
|
|
580
692
|
else if (type === PREFIX + 'set-input-method') {
|
|
581
|
-
|
|
693
|
+
// Live mode clamps the input method to pinpoint (what a plain click
|
|
694
|
+
// does); text drag-selection commenting stays live regardless.
|
|
695
|
+
currentInputMethod = (LIVE || e.data.method === 'pinpoint') ? 'pinpoint' : 'drag';
|
|
582
696
|
if (currentInputMethod === 'pinpoint') {
|
|
583
697
|
clearHoverHighlight(); // pinpoint owns clicks; drop the select affordance (and any pending hit test)
|
|
584
|
-
if (document.body) document.body.setAttribute('data-plannotator-pinpoint-cursor', '');
|
|
585
698
|
} else {
|
|
586
|
-
if (document.body) document.body.removeAttribute('data-plannotator-pinpoint-cursor');
|
|
587
699
|
clearPinpointHover();
|
|
588
700
|
}
|
|
701
|
+
updatePinpointCursor(); // cursor affordance is mode-gated: never in Interact
|
|
589
702
|
if (vimEnabled) updateVimUi();
|
|
590
703
|
}
|
|
591
704
|
|
|
705
|
+
else if (type === PREFIX + 'set-annotate-mode') {
|
|
706
|
+
var nextAnnotateActive = e.data.active === true;
|
|
707
|
+
if (nextAnnotateActive !== annotateModeActive) {
|
|
708
|
+
annotateModeActive = nextAnnotateActive;
|
|
709
|
+
if (!annotateModeActive) {
|
|
710
|
+
// Disarm tears down every PENDING affordance; committed markers and
|
|
711
|
+
// highlights stay visible, and marker buttons keep their clicks.
|
|
712
|
+
if (pendingSelection) postToParent({ type: PREFIX + 'selection-clear' });
|
|
713
|
+
pendingSelection = null;
|
|
714
|
+
pendingRange = null;
|
|
715
|
+
skipNextClear = false;
|
|
716
|
+
clearMultiTargets();
|
|
717
|
+
clearPendingPin();
|
|
718
|
+
try { window.getSelection().removeAllRanges(); } catch (ex) {}
|
|
719
|
+
clearPinpointHover();
|
|
720
|
+
clearHoverHighlight();
|
|
721
|
+
renderAnnotationOverlay();
|
|
722
|
+
}
|
|
723
|
+
updatePinpointCursor();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
592
727
|
else if (type === PREFIX + 'set-vim-mode') {
|
|
728
|
+
// Vim is off in live mode: it writes classes onto author elements and
|
|
729
|
+
// captures keys the app needs.
|
|
730
|
+
if (LIVE) return;
|
|
593
731
|
var wasVimEnabled = vimEnabled;
|
|
594
732
|
var wasVimHudEnabled = vimHudEnabled;
|
|
595
733
|
vimEnabled = e.data.enabled === true;
|
|
@@ -1096,6 +1234,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1096
1234
|
document.addEventListener('mousemove', function(e) {
|
|
1097
1235
|
lastPointer = { x: e.clientX, y: e.clientY };
|
|
1098
1236
|
updateDragYield(e);
|
|
1237
|
+
if (!annotateModeActive) return; // Interact mode: no hover affordances
|
|
1099
1238
|
if (currentInputMethod !== 'pinpoint') {
|
|
1100
1239
|
// Click-to-select hover affordance (drag mode owns highlight clicks):
|
|
1101
1240
|
// cheap cached-rect hit test, rAF-throttled, cleared while a draft or
|
|
@@ -1108,6 +1247,9 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1108
1247
|
return;
|
|
1109
1248
|
}
|
|
1110
1249
|
if (vimEnabled && vimPhase !== 'inactive') return;
|
|
1250
|
+
// Mid-drag (text selection in progress) the element hover box is noise:
|
|
1251
|
+
// the drag owns the surface until mouseup resolves it.
|
|
1252
|
+
if (dragYieldActive) { clearPinpointHover(); return; }
|
|
1111
1253
|
// Hit-test at the pointer (e.target only backstops engines without
|
|
1112
1254
|
// elementFromPoint) so the same code path serves the scroll re-hit-test.
|
|
1113
1255
|
updatePinpointHover(e.clientX, e.clientY, e.target);
|
|
@@ -1137,7 +1279,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1137
1279
|
renderAnnotationOverlay();
|
|
1138
1280
|
// Scroll under a stationary pointer moves the committed rects: re-run
|
|
1139
1281
|
// the cached-rect hover test so the affordance tracks reality.
|
|
1140
|
-
if (currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
|
|
1282
|
+
if (annotateModeActive && currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
|
|
1141
1283
|
setHoverHighlight(committedHighlightIdAtCached(lastPointer.x, lastPointer.y));
|
|
1142
1284
|
}
|
|
1143
1285
|
positionMultiTargetBoxes();
|
|
@@ -1145,7 +1287,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1145
1287
|
positionPinpointBox(pendingPinEl);
|
|
1146
1288
|
return;
|
|
1147
1289
|
}
|
|
1148
|
-
if (currentInputMethod !== 'pinpoint') return;
|
|
1290
|
+
if (!annotateModeActive || currentInputMethod !== 'pinpoint') return;
|
|
1149
1291
|
if (vimEnabled && vimPhase !== 'inactive') return;
|
|
1150
1292
|
if (lastPointer) {
|
|
1151
1293
|
updatePinpointHover(lastPointer.x, lastPointer.y, pinpointHover);
|
|
@@ -1259,15 +1401,21 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1259
1401
|
}
|
|
1260
1402
|
}
|
|
1261
1403
|
|
|
1262
|
-
// Drag-selection marker yield: while a text drag is in progress
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1404
|
+
// Drag-selection marker yield: while a text drag is in progress, placed
|
|
1405
|
+
// markers drop pointer input (the same data-pn-hittest CSS the hit-test
|
|
1406
|
+
// yield uses) so a 25px bubble sitting over the text cannot capture the
|
|
1407
|
+
// selection mid-drag. Armed only by a >4px move with the primary button
|
|
1408
|
+
// held from a non-overlay mousedown, so marker clicks (mousedown ON the
|
|
1409
|
+
// marker) and plain click-to-select (no drag) are untouched; disarmed on
|
|
1410
|
+
// mouseup or when the button is seen released. The same >4px arming is
|
|
1411
|
+
// what tells the mouseup handler below that a REAL drag happened, which
|
|
1412
|
+
// is how drag-selection commenting stays live in pinpoint/armed mode
|
|
1413
|
+
// without a plain pinpoint click re-posting its own selection.
|
|
1269
1414
|
var dragYieldStart = null;
|
|
1270
1415
|
var dragYieldActive = false;
|
|
1416
|
+
// True between a drag's terminating mouseup and the next mousedown: the
|
|
1417
|
+
// click event that follows a completed drag must not pinpoint-annotate.
|
|
1418
|
+
var dragEndedClick = false;
|
|
1271
1419
|
function endDragYield() {
|
|
1272
1420
|
dragYieldStart = null;
|
|
1273
1421
|
if (dragYieldActive) {
|
|
@@ -1292,13 +1440,37 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1292
1440
|
}
|
|
1293
1441
|
}
|
|
1294
1442
|
document.addEventListener('mousedown', function(e) {
|
|
1295
|
-
|
|
1443
|
+
dragEndedClick = false;
|
|
1296
1444
|
if (e.button !== 0) return;
|
|
1297
1445
|
if (isViewerOverlayNode(e.target)) return;
|
|
1298
1446
|
dragYieldStart = { x: e.clientX, y: e.clientY };
|
|
1299
1447
|
}, true);
|
|
1300
1448
|
document.addEventListener('mouseup', function() {
|
|
1449
|
+
// Text drag-selection commenting is ALWAYS live: both surfaces, armed or
|
|
1450
|
+
// Interact. In armed pinpoint a plain click belongs to the pinpoint
|
|
1451
|
+
// handler — only a drag that actually PRODUCED a text selection owns its
|
|
1452
|
+
// trailing click, so annotateElement's own selection is never re-posted
|
|
1453
|
+
// by its trailing mouseup. The >4px drift alone is NOT enough to arm the
|
|
1454
|
+
// click suppression: a drifted click (common on trackpads) that selected
|
|
1455
|
+
// nothing must stay a click, or armed pinpoint would silently swallow it
|
|
1456
|
+
// while the unprevented click leaks through to the page. Browsers clear
|
|
1457
|
+
// a prior selection on the mousedown that starts the drag, so the
|
|
1458
|
+
// selection observed here is the one THIS drag produced. Everywhere else
|
|
1459
|
+
// the classic always-schedule behavior stays: handleSelection only acts
|
|
1460
|
+
// on a real selection, posts the clear that dismisses a stale draft, and
|
|
1461
|
+
// never preventDefaults — a plain click is never swallowed.
|
|
1462
|
+
var dragged = dragYieldActive;
|
|
1301
1463
|
endDragYield();
|
|
1464
|
+
var draggedSelection = false;
|
|
1465
|
+
if (dragged) {
|
|
1466
|
+
try {
|
|
1467
|
+
var s = window.getSelection();
|
|
1468
|
+
draggedSelection = !!(s && !s.isCollapsed && s.rangeCount && (s.toString() || '').trim());
|
|
1469
|
+
} catch (ex) {}
|
|
1470
|
+
}
|
|
1471
|
+
dragEndedClick = draggedSelection;
|
|
1472
|
+
if (annotateModeActive && currentInputMethod === 'pinpoint' && !draggedSelection) return;
|
|
1473
|
+
setTimeout(handleSelection, 10);
|
|
1302
1474
|
}, true);
|
|
1303
1475
|
|
|
1304
1476
|
// One record per committed annotation. Its targets are live projections;
|
|
@@ -1342,6 +1514,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1342
1514
|
// whose records are removed and therefore invisible to the per-pass scan.
|
|
1343
1515
|
var lastUnanchoredKey = '[]';
|
|
1344
1516
|
var restoreFailedIds = new Set();
|
|
1517
|
+
// Set by report-unanchored: the parent asks for the complete set after
|
|
1518
|
+
// its restore batch, so the next COMPLETE pass emits even when the set
|
|
1519
|
+
// did not change (an all-restored document reports its empty set once).
|
|
1520
|
+
var unanchoredReportRequested = false;
|
|
1345
1521
|
function emitUnanchored(deadRecordIds) {
|
|
1346
1522
|
var seen = new Set();
|
|
1347
1523
|
var combined = [];
|
|
@@ -1360,9 +1536,14 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1360
1536
|
combined.sort();
|
|
1361
1537
|
if (combined.length > 512) combined = combined.slice(0, 512);
|
|
1362
1538
|
var key = JSON.stringify(combined);
|
|
1363
|
-
if (key === lastUnanchoredKey) return;
|
|
1539
|
+
if (key === lastUnanchoredKey && !unanchoredReportRequested) return;
|
|
1540
|
+
unanchoredReportRequested = false;
|
|
1364
1541
|
lastUnanchoredKey = key;
|
|
1365
|
-
|
|
1542
|
+
// postToParent, not a raw '*' post: live sessions stamp the session
|
|
1543
|
+
// token and post only to the listed editor origins, and the parent
|
|
1544
|
+
// drops untokened live messages — a raw post would silently disable
|
|
1545
|
+
// unanchored reporting exactly where restores fail most (live pages).
|
|
1546
|
+
postToParent({ type: PREFIX + 'unanchored', ids: combined });
|
|
1366
1547
|
}
|
|
1367
1548
|
|
|
1368
1549
|
function validNormalizedPoint(p) {
|
|
@@ -1502,9 +1683,47 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1502
1683
|
}
|
|
1503
1684
|
}
|
|
1504
1685
|
}
|
|
1686
|
+
if (!record.targets.length && LIVE) {
|
|
1687
|
+
// Live pages render late (lazy routes, data-dependent trees): a
|
|
1688
|
+
// restore that resolves nothing keeps its record, seeded with
|
|
1689
|
+
// unresolved placeholder targets built from the durable params, so
|
|
1690
|
+
// the mutation-driven reconcile re-acquires them when the elements
|
|
1691
|
+
// appear. Srcdoc documents are static (nothing would ever
|
|
1692
|
+
// re-resolve), so the record is dropped below exactly as before.
|
|
1693
|
+
if (anchor) {
|
|
1694
|
+
record.targets.push({
|
|
1695
|
+
kind: 'element',
|
|
1696
|
+
element: null,
|
|
1697
|
+
anchor: anchor,
|
|
1698
|
+
point: normalizedPointOf(anchor, null)
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
if (originalText) {
|
|
1702
|
+
record.targets.push({
|
|
1703
|
+
kind: 'range',
|
|
1704
|
+
range: null,
|
|
1705
|
+
text: originalText,
|
|
1706
|
+
markerless: !!anchor
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
if (additionalAnchors && additionalAnchors.length) {
|
|
1710
|
+
var lateCount = Math.min(additionalAnchors.length, MAX_MULTI_TARGETS);
|
|
1711
|
+
for (var lateIndex = 0; lateIndex < lateCount; lateIndex++) {
|
|
1712
|
+
if (!additionalAnchors[lateIndex]) continue;
|
|
1713
|
+
record.targets.push({
|
|
1714
|
+
kind: 'element',
|
|
1715
|
+
element: null,
|
|
1716
|
+
anchor: additionalAnchors[lateIndex],
|
|
1717
|
+
point: normalizedPointOf(additionalAnchors[lateIndex], null)
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1505
1722
|
if (!record.targets.length) {
|
|
1506
1723
|
// The record is removed (nothing to retry), so the per-pass dead scan
|
|
1507
1724
|
// cannot see this id: track it separately for the unanchored report.
|
|
1725
|
+
// Live sessions only reach here when the durable params seeded no
|
|
1726
|
+
// placeholder targets at all (nothing will ever re-resolve).
|
|
1508
1727
|
removeAnnRecord(id);
|
|
1509
1728
|
restoreFailedIds.add(id);
|
|
1510
1729
|
}
|
|
@@ -2067,7 +2286,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2067
2286
|
btn.addEventListener('click', function(clickEvent) {
|
|
2068
2287
|
clickEvent.preventDefault();
|
|
2069
2288
|
clickEvent.stopPropagation();
|
|
2070
|
-
|
|
2289
|
+
postToParent({ type: PREFIX + 'mark-click', id: annId });
|
|
2071
2290
|
});
|
|
2072
2291
|
overlayNodes.add(btn);
|
|
2073
2292
|
return btn;
|
|
@@ -2307,7 +2526,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2307
2526
|
}
|
|
2308
2527
|
}
|
|
2309
2528
|
|
|
2310
|
-
function scrollToAnnotation(id) {
|
|
2529
|
+
function scrollToAnnotation(id, behavior) {
|
|
2311
2530
|
var record = findAnnRecord(id);
|
|
2312
2531
|
if (!record) return;
|
|
2313
2532
|
beginDeadSearchPass(Infinity); // user-initiated one-shot: never budget-starved
|
|
@@ -2323,7 +2542,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2323
2542
|
}
|
|
2324
2543
|
}
|
|
2325
2544
|
if (scrollEl) {
|
|
2326
|
-
try { scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) {}
|
|
2545
|
+
try { scrollEl.scrollIntoView({ behavior: behavior || 'smooth', block: 'center' }); } catch (ex) {}
|
|
2327
2546
|
}
|
|
2328
2547
|
focusAnnotationRecord(id, true);
|
|
2329
2548
|
}
|
|
@@ -2505,6 +2724,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2505
2724
|
pendingPinPoint = null;
|
|
2506
2725
|
pendingPinViaPinpoint = false;
|
|
2507
2726
|
multiSelectArmed = false;
|
|
2727
|
+
multiSelectMax = MAX_MULTI_TARGETS;
|
|
2508
2728
|
hidePinpointBox();
|
|
2509
2729
|
}
|
|
2510
2730
|
|
|
@@ -2608,7 +2828,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2608
2828
|
clearPendingPin();
|
|
2609
2829
|
try { window.getSelection().removeAllRanges(); } catch (ex) {}
|
|
2610
2830
|
renderAnnotationOverlay();
|
|
2611
|
-
if (echo)
|
|
2831
|
+
if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
|
|
2612
2832
|
return;
|
|
2613
2833
|
}
|
|
2614
2834
|
var next = pendingMultiTargets.shift();
|
|
@@ -2628,14 +2848,14 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2628
2848
|
mainBox.classList.remove('pn-pin-enter');
|
|
2629
2849
|
if (pendingPinEl && pendingPinEl.isConnected) positionPinpointBox(pendingPinEl);
|
|
2630
2850
|
renderAnnotationOverlay();
|
|
2631
|
-
if (echo)
|
|
2851
|
+
if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
|
|
2632
2852
|
return;
|
|
2633
2853
|
}
|
|
2634
2854
|
for (var i = 0; i < pendingMultiTargets.length; i++) {
|
|
2635
2855
|
if (pendingMultiTargets[i].key === key) {
|
|
2636
2856
|
destroyMultiTargetBox(pendingMultiTargets[i].box);
|
|
2637
2857
|
pendingMultiTargets.splice(i, 1);
|
|
2638
|
-
if (echo)
|
|
2858
|
+
if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
|
|
2639
2859
|
return;
|
|
2640
2860
|
}
|
|
2641
2861
|
}
|
|
@@ -2669,8 +2889,9 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2669
2889
|
}
|
|
2670
2890
|
}
|
|
2671
2891
|
}
|
|
2672
|
-
// Cap at the source: never grow the draft past the parent-side DTO cap
|
|
2673
|
-
|
|
2892
|
+
// Cap at the source: never grow the draft past the parent-side DTO cap
|
|
2893
|
+
// (or the lower product cap the parent armed this draft with).
|
|
2894
|
+
if (pendingMultiTargets.length >= multiSelectMax) return;
|
|
2674
2895
|
var point = normalizePointInElement(el, clickPoint);
|
|
2675
2896
|
if (anchor && point) anchor.point = point;
|
|
2676
2897
|
var label = pinpointHoverLabel(el);
|
|
@@ -2678,13 +2899,13 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2678
2899
|
var key = makeTargetKey();
|
|
2679
2900
|
var box = createMultiTargetBox(el);
|
|
2680
2901
|
pendingMultiTargets.push({ key: key, el: el, anchor: anchor, label: label, text: text, point: point, box: box });
|
|
2681
|
-
|
|
2902
|
+
postToParent({
|
|
2682
2903
|
type: PREFIX + 'multi-target-added',
|
|
2683
2904
|
key: key,
|
|
2684
2905
|
label: label,
|
|
2685
2906
|
text: text,
|
|
2686
2907
|
anchor: anchor || undefined
|
|
2687
|
-
}
|
|
2908
|
+
});
|
|
2688
2909
|
}
|
|
2689
2910
|
|
|
2690
2911
|
/** Chip hover in the composer: flash the corresponding pinned outline. */
|
|
@@ -2748,12 +2969,12 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2748
2969
|
pointerRelayRaf = requestAnimationFrame(function() {
|
|
2749
2970
|
pointerRelayRaf = 0;
|
|
2750
2971
|
if (!pendingPinEl || !pointerRelayPos) return;
|
|
2751
|
-
|
|
2972
|
+
postToParent({
|
|
2752
2973
|
type: PREFIX + 'pointer',
|
|
2753
2974
|
x: pointerRelayPos.x,
|
|
2754
2975
|
y: pointerRelayPos.y,
|
|
2755
2976
|
shift: pointerRelayPos.shift
|
|
2756
|
-
}
|
|
2977
|
+
});
|
|
2757
2978
|
});
|
|
2758
2979
|
}
|
|
2759
2980
|
|
|
@@ -2773,6 +2994,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2773
2994
|
// drafts (comment -> quick label) leaves a stale arm and the bridge
|
|
2774
2995
|
// accumulates pins the saved annotation will not carry.
|
|
2775
2996
|
multiSelectArmed = false;
|
|
2997
|
+
multiSelectMax = MAX_MULTI_TARGETS;
|
|
2776
2998
|
pendingPinEl = el;
|
|
2777
2999
|
pendingPinAnchor = buildElementAnchor(el);
|
|
2778
3000
|
pendingPinKey = makeTargetKey();
|
|
@@ -2823,22 +3045,31 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2823
3045
|
pendingSelection = { element: true };
|
|
2824
3046
|
pendingRange = null;
|
|
2825
3047
|
skipNextClear = true; // don't let this click's mouseup clear the toolbar we just opened
|
|
2826
|
-
|
|
3048
|
+
postToParent({ type: PREFIX + 'selection', text: elText,
|
|
2827
3049
|
modeOverride: modeOverride || undefined,
|
|
2828
3050
|
anchor: pendingPinAnchor || undefined,
|
|
2829
3051
|
pinpoint: !!viaPinpoint || undefined,
|
|
2830
3052
|
targetKey: pendingPinKey || undefined,
|
|
2831
3053
|
targetLabel: pendingPinLabel || undefined,
|
|
2832
|
-
rect: { top: r.top, left: r.left, width: r.width, height: r.height } }
|
|
3054
|
+
rect: { top: r.top, left: r.left, width: r.width, height: r.height } });
|
|
2833
3055
|
return true;
|
|
2834
3056
|
}
|
|
2835
3057
|
|
|
2836
3058
|
document.addEventListener('click', function(e) {
|
|
2837
|
-
if (currentInputMethod !== 'pinpoint') return;
|
|
3059
|
+
if (!annotateModeActive || currentInputMethod !== 'pinpoint') return;
|
|
2838
3060
|
// Real placed markers (and any other viewer overlay) own their clicks —
|
|
2839
3061
|
// checked by IDENTITY, not selector, so a page element spoofing
|
|
2840
3062
|
// [data-plannotator-marker] stays an ordinary annotatable target.
|
|
2841
3063
|
if (isViewerOverlayNode(e.target)) return;
|
|
3064
|
+
// A drag that ended in this click owns the surface: the drag-selection
|
|
3065
|
+
// pass is about to post the selected text, and pinpoint-annotating the
|
|
3066
|
+
// element under the pointer would clobber it (annotateElement rewrites
|
|
3067
|
+
// the selection). Armed at mouseup only when the drag actually produced
|
|
3068
|
+
// a selection — a drifted click arms nothing and pins normally below —
|
|
3069
|
+
// and mousedown clears a prior pin's leftover selection, so stale
|
|
3070
|
+
// selection state never blocks the next plain re-pin click. One-shot:
|
|
3071
|
+
// only the drag's own trailing click is suppressed.
|
|
3072
|
+
if (dragEndedClick) { dragEndedClick = false; return; }
|
|
2842
3073
|
// Shift-click while an ARMED pinpoint draft is open: toggle the element
|
|
2843
3074
|
// in/out of the SAME draft comment instead of replacing the selection.
|
|
2844
3075
|
// Unarmed drafts (modes the parent does not mirror, e.g. quickLabel)
|
|
@@ -2865,24 +3096,51 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2865
3096
|
annotateElement(el, undefined, true, { x: e.clientX, y: e.clientY });
|
|
2866
3097
|
}, true);
|
|
2867
3098
|
|
|
2868
|
-
// Escape
|
|
2869
|
-
//
|
|
3099
|
+
// Escape ladder (outside vim, which has its own): a pending draft closes
|
|
3100
|
+
// first, then the hover outline clears, then Esc EXITS Annotate back to
|
|
3101
|
+
// Interact — the parent owns the mode, so the final rung only posts
|
|
3102
|
+
// annotate-exit and waits for set-annotate-mode to come back down.
|
|
3103
|
+
function closePendingDraft() {
|
|
3104
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
3105
|
+
pendingSelection = null;
|
|
3106
|
+
pendingRange = null;
|
|
3107
|
+
skipNextClear = false;
|
|
3108
|
+
clearMultiTargets();
|
|
3109
|
+
clearPendingPin();
|
|
3110
|
+
window.getSelection().removeAllRanges();
|
|
3111
|
+
renderAnnotationOverlay();
|
|
3112
|
+
}
|
|
2870
3113
|
document.addEventListener('keydown', function(e) {
|
|
2871
3114
|
if (e.key !== 'Escape' || vimEnabled) return;
|
|
3115
|
+
if (!annotateModeActive) {
|
|
3116
|
+
// Interact mode: Esc belongs to the page — except an open drag-comment
|
|
3117
|
+
// draft (drag-selection stays live in Interact) still closes first.
|
|
3118
|
+
if (pendingSelection) closePendingDraft();
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
2872
3121
|
if (pendingSelection) {
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
} else if (currentInputMethod === 'pinpoint') {
|
|
2882
|
-
clearPinpointHover();
|
|
3122
|
+
closePendingDraft();
|
|
3123
|
+
} else {
|
|
3124
|
+
// Hover-clear is NOT a rung: the hover outline is a pointer
|
|
3125
|
+
// affordance, not a state the user perceives as a step, so clearing
|
|
3126
|
+
// it and exiting to Interact happen on the SAME press. Only an open
|
|
3127
|
+
// draft earns its own press.
|
|
3128
|
+
if (currentInputMethod === 'pinpoint' && pinpointHover) clearPinpointHover();
|
|
3129
|
+
postToParent({ type: PREFIX + 'annotate-exit' });
|
|
2883
3130
|
}
|
|
2884
3131
|
});
|
|
2885
3132
|
|
|
3133
|
+
// Mod+Shift+A toggles Interact/Annotate from inside the iframe (the parent
|
|
3134
|
+
// registers the same chord, but focus usually lives in here on live apps).
|
|
3135
|
+
// Capture phase so the page cannot swallow the reserved chord; the parent
|
|
3136
|
+
// answers with set-annotate-mode.
|
|
3137
|
+
document.addEventListener('keydown', function(e) {
|
|
3138
|
+
if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.altKey) return;
|
|
3139
|
+
if (e.key !== 'a' && e.key !== 'A') return;
|
|
3140
|
+
e.preventDefault();
|
|
3141
|
+
postToParent({ type: PREFIX + 'annotate-toggle' });
|
|
3142
|
+
}, true);
|
|
3143
|
+
|
|
2886
3144
|
// Author opt-in: a plain click on any element tagged [data-annotate] pops the
|
|
2887
3145
|
// toolbar — no pinpoint mode. Lets an HTML doc (e.g. a flow graph) wire its own
|
|
2888
3146
|
// nodes to Plannotator's toolbar. Bubble phase so the page's own click handlers
|
|
@@ -2890,6 +3148,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2890
3148
|
// a committed highlight selects the annotation instead (the pre-overlay
|
|
2891
3149
|
// handler deferred to '.annotation-highlight' the same way).
|
|
2892
3150
|
document.addEventListener('click', function(e) {
|
|
3151
|
+
if (!annotateModeActive) return; // Interact mode: [data-annotate] stays a page element
|
|
2893
3152
|
if (currentInputMethod === 'pinpoint') return; // pinpoint handler covers this
|
|
2894
3153
|
if (isViewerOverlayNode(e.target)) return; // placed markers own their clicks
|
|
2895
3154
|
var t = e.target && e.target.closest && e.target.closest('[data-annotate]');
|
|
@@ -2933,6 +3192,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2933
3192
|
}
|
|
2934
3193
|
|
|
2935
3194
|
document.addEventListener('click', function(e) {
|
|
3195
|
+
// Interact mode: highlight rects are pointer-transparent projections, so a
|
|
3196
|
+
// page click landing on one goes to the page. Markers (real buttons) stay
|
|
3197
|
+
// the affordance for opening a committed comment in Interact.
|
|
3198
|
+
if (!annotateModeActive) return;
|
|
2936
3199
|
if (e.shiftKey) return; // shift belongs to multi-select
|
|
2937
3200
|
if (isViewerOverlayNode(e.target)) return; // markers own their clicks
|
|
2938
3201
|
if (pendingPinEl) return; // an open pinpoint draft owns the surface
|
|
@@ -2941,7 +3204,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2941
3204
|
var hitId = committedHighlightAt(e.clientX, e.clientY);
|
|
2942
3205
|
if (!hitId) return;
|
|
2943
3206
|
e.stopPropagation();
|
|
2944
|
-
|
|
3207
|
+
postToParent({ type: PREFIX + 'mark-click', id: hitId });
|
|
2945
3208
|
});
|
|
2946
3209
|
|
|
2947
3210
|
// --- Optional Vim navigation ---
|
|
@@ -3655,10 +3918,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3655
3918
|
if (!vimEnabled) return;
|
|
3656
3919
|
if (vimHudEnabled && vimLastPostedPhase !== vimPhase) {
|
|
3657
3920
|
vimLastPostedPhase = vimPhase;
|
|
3658
|
-
|
|
3921
|
+
postToParent({
|
|
3659
3922
|
type: PREFIX + 'vim-state',
|
|
3660
3923
|
phase: vimPhase
|
|
3661
|
-
}
|
|
3924
|
+
});
|
|
3662
3925
|
}
|
|
3663
3926
|
var badge = document.querySelector('[data-plannotator-vim-badge]');
|
|
3664
3927
|
if (!vimHudEnabled && !badge) badge = getVimBadgeEl();
|
|
@@ -3720,10 +3983,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3720
3983
|
|
|
3721
3984
|
function toggleVimHelp() {
|
|
3722
3985
|
vimHelpOpen = !vimHelpOpen;
|
|
3723
|
-
|
|
3986
|
+
postToParent({
|
|
3724
3987
|
type: PREFIX + 'vim-help',
|
|
3725
3988
|
open: vimHelpOpen
|
|
3726
|
-
}
|
|
3989
|
+
});
|
|
3727
3990
|
}
|
|
3728
3991
|
|
|
3729
3992
|
function clearVimUi() {
|
|
@@ -3743,10 +4006,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3743
4006
|
|
|
3744
4007
|
function copyVimText(text) {
|
|
3745
4008
|
if (!text) return;
|
|
3746
|
-
|
|
4009
|
+
postToParent({
|
|
3747
4010
|
type: PREFIX + 'vim-copy',
|
|
3748
4011
|
text: text
|
|
3749
|
-
}
|
|
4012
|
+
});
|
|
3750
4013
|
}
|
|
3751
4014
|
|
|
3752
4015
|
function vimActionMode(key) {
|
|
@@ -3808,7 +4071,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3808
4071
|
handled = true;
|
|
3809
4072
|
} else if (key === 'Escape') {
|
|
3810
4073
|
if (pendingSelection) {
|
|
3811
|
-
|
|
4074
|
+
postToParent({ type: PREFIX + 'selection-clear' });
|
|
3812
4075
|
pendingSelection = null;
|
|
3813
4076
|
pendingRange = null;
|
|
3814
4077
|
restoreVimSemanticTarget();
|
|
@@ -4025,12 +4288,12 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4025
4288
|
vimLastActionId = vimActionId;
|
|
4026
4289
|
vimLastActionContext = vimCommandContext;
|
|
4027
4290
|
updateVimReticle();
|
|
4028
|
-
|
|
4291
|
+
postToParent({
|
|
4029
4292
|
type: PREFIX + 'vim-command',
|
|
4030
4293
|
actionId: vimActionId,
|
|
4031
4294
|
key: hudKey,
|
|
4032
4295
|
context: vimCommandContext
|
|
4033
|
-
}
|
|
4296
|
+
});
|
|
4034
4297
|
}
|
|
4035
4298
|
e.preventDefault();
|
|
4036
4299
|
e.stopImmediatePropagation();
|
|
@@ -4064,7 +4327,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4064
4327
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
4065
4328
|
if (!e.key || e.key.length !== 1) return; // single printable char only
|
|
4066
4329
|
e.preventDefault();
|
|
4067
|
-
|
|
4330
|
+
postToParent({ type: PREFIX + 'keytype', key: e.key });
|
|
4068
4331
|
// Hand keyboard focus back to the parent window so the comment textarea can
|
|
4069
4332
|
// take it. Blurring the <iframe> from the parent isn't enough — the inner
|
|
4070
4333
|
// document keeps focus — so the iframe must relinquish it. parent.focus() is
|
|
@@ -4305,7 +4568,13 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4305
4568
|
}).observe(document.body);
|
|
4306
4569
|
}
|
|
4307
4570
|
watchPageMutations();
|
|
4308
|
-
|
|
4571
|
+
// Armed is the default on both surfaces, and live sessions default to
|
|
4572
|
+
// pinpoint: show the cursor affordance immediately instead of waiting for
|
|
4573
|
+
// the parent's first set-input-method/set-annotate-mode round trip.
|
|
4574
|
+
updatePinpointCursor();
|
|
4575
|
+
var readyMsg = { type: PREFIX + 'ready' };
|
|
4576
|
+
if (LIVE) readyMsg.pageUrl = currentPageUrl();
|
|
4577
|
+
postToParent(readyMsg);
|
|
4309
4578
|
}
|
|
4310
4579
|
if (document.readyState === 'loading') {
|
|
4311
4580
|
document.addEventListener('DOMContentLoaded', onReady);
|
|
@@ -4333,3 +4602,22 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4333
4602
|
}
|
|
4334
4603
|
};
|
|
4335
4604
|
})();`;
|
|
4605
|
+
|
|
4606
|
+
/**
|
|
4607
|
+
* Live-mode bootstrap, prepended to BRIDGE_SCRIPT by the annotate server when
|
|
4608
|
+
* composing the proxy-served bridge body. Reads the JSON config prelude
|
|
4609
|
+
* (window.__plannotatorLiveConfig) and installs the annotation CSS that srcdoc
|
|
4610
|
+
* mode splices as a <style> tag. Runs before the bridge IIFE and before its
|
|
4611
|
+
* MutationObserver exists, so this write never feeds the reconcile loop.
|
|
4612
|
+
* Same escaping rules as BRIDGE_SCRIPT: a dependency-free string constant.
|
|
4613
|
+
*/
|
|
4614
|
+
export const LIVE_BRIDGE_BOOTSTRAP = `(function() {
|
|
4615
|
+
var config = window.__plannotatorLiveConfig;
|
|
4616
|
+
if (!config || typeof config.css !== 'string') return;
|
|
4617
|
+
try {
|
|
4618
|
+
var style = document.createElement('style');
|
|
4619
|
+
style.setAttribute('data-plannotator-live-css', '');
|
|
4620
|
+
style.appendChild(document.createTextNode(config.css));
|
|
4621
|
+
(document.head || document.documentElement).appendChild(style);
|
|
4622
|
+
} catch (ex) {}
|
|
4623
|
+
})();`;
|