@plannotator/ui 0.53.0 → 0.54.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/HANDOFF.md +284 -27
- package/README.md +84 -14
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +177 -34
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- package/components/AnnotationToolstrip.tsx +45 -40
- package/components/ApproveDropdown.tsx +7 -9
- package/components/AskSessionAnnouncementDialog.tsx +1 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/BlockRenderer.tsx +19 -5
- package/components/CodeFilePopout.tsx +140 -5
- package/components/CommentPopover.tsx +46 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DiagramBlock.tsx +17 -12
- package/components/EditorAnnotationCard.tsx +1 -1
- package/components/ExportModal.tsx +77 -7
- package/components/HostTitle.tsx +0 -4
- package/components/HtmlSurfaceControls.tsx +2 -1
- package/components/ImageAnnotator/Toolbar.tsx +25 -1
- package/components/ImageAnnotator/index.tsx +24 -4
- package/components/ImageLightbox.tsx +658 -120
- package/components/ImagePeek.tsx +38 -4
- package/components/ImageRegions.tsx +369 -0
- package/components/ImageThumbnail.tsx +1 -1
- package/components/ImageZoomStage.tsx +796 -0
- package/components/InboxAnnouncementDialog.tsx +202 -0
- package/components/InlineMarkdown.tsx +152 -24
- package/components/KeyboardShortcuts.tsx +23 -0
- package/components/LazyBlockBoundary.tsx +100 -0
- package/components/ListMarker.tsx +3 -3
- package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
- package/components/MentionPicker.tsx +51 -0
- package/components/MenuVersionSection.tsx +31 -9
- package/components/PlanDecisionMenuExtras.tsx +129 -0
- package/components/PlanHeaderMenu.tsx +15 -1
- package/components/ProviderIcons.tsx +8 -0
- package/components/RefHoverCard.tsx +913 -0
- package/components/SendFailureToast.tsx +126 -0
- package/components/Settings.tsx +617 -126
- package/components/SkillReferenceMenu.tsx +39 -12
- package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
- package/components/ThemeProvider.tsx +14 -1
- package/components/ThemeTab.tsx +16 -24
- package/components/Tooltip.tsx +49 -18
- package/components/Viewer.tsx +316 -47
- package/components/ai/DocumentAIChatPanel.tsx +29 -3
- package/components/ai/NewChatButton.tsx +74 -0
- package/components/blocks/CodeBlock.tsx +1 -1
- package/components/blocks/HtmlBlock.tsx +7 -1
- package/components/blocks/QuestionBlock.tsx +522 -62
- package/components/blocks/QuestionContext.tsx +3 -0
- package/components/diagram/DiagramComposer.tsx +5 -1
- package/components/diagram/DiagramPending.tsx +12 -2
- package/components/diagram/DiagramViewer.tsx +19 -3
- package/components/html-embed/HtmlEmbedBlock.tsx +374 -0
- package/components/html-embed/SvgFenceBlock.tsx +46 -0
- package/components/html-embed/index.ts +3 -0
- package/components/html-embed/languages.ts +15 -0
- package/components/html-embed/loadGate.ts +107 -0
- package/components/html-viewer/HtmlViewer.tsx +69 -6
- package/components/html-viewer/bridge-script.asset.js +504 -12
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +545 -12
- package/components/html-viewer/useHtmlAnnotation.ts +125 -0
- package/components/imageZoom.ts +265 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
- package/components/plan-diff/PlanDiffViewer.tsx +13 -2
- package/components/plan-diff/PlanRawDiffView.tsx +1 -1
- package/components/sidebar/FileBrowser.tsx +16 -2
- package/components/sidebar/SidebarTabs.tsx +3 -0
- package/config/index.ts +3 -0
- package/config/reviewView.ts +41 -1
- package/config/settings.ts +155 -4
- package/configure.ts +28 -0
- package/hooks/pfm/useCodeFilePopout.ts +7 -1
- package/hooks/useAIChat.ts +72 -25
- package/hooks/useAnnotationDraft.ts +71 -6
- package/hooks/useAnnotationHighlighter.ts +57 -3
- package/hooks/useCodeAnnotationDraft.ts +113 -17
- package/hooks/useCookieHostSetting.ts +96 -0
- package/hooks/useDocumentFont.ts +17 -0
- package/hooks/useDocumentTextSize.ts +20 -0
- package/hooks/useFileBrowser.ts +10 -1
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/useMentionAutocomplete.ts +1 -1
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSkillReferenceAutocomplete.ts +7 -3
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/hooks/useVimSelection.ts +9 -0
- package/package.json +23 -22
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
- package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
- package/shortcuts/core.ts +63 -4
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
- package/shortcuts/runtime.ts +14 -6
- package/styles.css +1 -1
- package/theme.css +179 -1
- package/themes/plannotator-graphite.css +66 -0
- package/types.ts +26 -9
- package/utils/agentSwitch.ts +41 -20
- package/utils/agentToolSetting.ts +1 -1
- package/utils/aiChatFormat.ts +23 -7
- package/utils/annotationOwnsHighlight.ts +4 -2
- package/utils/asciidocParser.ts +570 -0
- package/utils/autoUpdateNotice.ts +24 -0
- package/utils/belowTitleSlot.ts +174 -0
- package/utils/codeLineScroll.ts +100 -0
- package/utils/commentSubmitKey.ts +48 -0
- package/utils/cssFontFamily.ts +65 -0
- package/utils/decisionSpec.ts +190 -2
- package/utils/defaultNotesApp.ts +1 -1
- package/utils/diffFonts.ts +4 -1
- package/utils/documentFont.ts +68 -0
- package/utils/documentTextSize.ts +54 -0
- package/utils/fontDetect.ts +63 -0
- package/utils/inboxAnnouncement.ts +78 -0
- package/utils/lookAndFeelAnnouncement.ts +82 -5
- package/utils/markdownExtensions.ts +2 -2
- package/utils/mentions.ts +34 -5
- package/utils/mermaid.ts +20 -3
- package/utils/mermaidTheme.ts +3 -51
- package/utils/notion.ts +58 -0
- package/utils/parser.ts +28 -1
- package/utils/platform.ts +46 -0
- package/utils/refCard.ts +82 -0
- package/utils/richClipboard.ts +380 -0
- package/utils/richClipboardLists.ts +57 -0
- package/utils/richCopyPolicy.ts +22 -0
- package/utils/scrollAnchor.ts +82 -0
- package/utils/sectionRefs.ts +123 -0
- package/utils/selectionBoundary.ts +40 -0
- package/utils/sendFailure.ts +85 -0
- package/utils/sharing.ts +5 -2
- package/utils/themeRegistry.ts +10 -0
- package/utils/themeTokens.ts +74 -0
- package/utils/updateNotice.ts +30 -8
- package/utils/vscodePanel.ts +9 -0
|
@@ -183,6 +183,10 @@
|
|
|
183
183
|
// --- Selection ---
|
|
184
184
|
var pendingSelection = null;
|
|
185
185
|
var pendingRange = null; // live range for the pending selection (scroll tracking)
|
|
186
|
+
// The pending text selection's own text, uncapped: what plain-text Copy
|
|
187
|
+
// writes. Range.toString() would lose the rendered line breaks between
|
|
188
|
+
// blocks and keep the source indentation.
|
|
189
|
+
var pendingCopyText = '';
|
|
186
190
|
var pendingPinEl = null; // element pinned by a pinpoint click (outline + scroll tracking)
|
|
187
191
|
var pendingPinAnchor = null; // serialized anchor for the pending pin
|
|
188
192
|
var pendingPinKey = null; // target key for the primary pinpoint target (multi-select)
|
|
@@ -294,7 +298,8 @@
|
|
|
294
298
|
}
|
|
295
299
|
skipNextClear = false; // a real text selection happened
|
|
296
300
|
var range = sel.getRangeAt(0);
|
|
297
|
-
var
|
|
301
|
+
var fullText = sel.toString().trim();
|
|
302
|
+
var text = capSelectionText(fullText);
|
|
298
303
|
if (!text) return false;
|
|
299
304
|
|
|
300
305
|
// A draft now owns the surface: kill the click-to-select hover
|
|
@@ -303,6 +308,7 @@
|
|
|
303
308
|
|
|
304
309
|
var rect = range.getBoundingClientRect();
|
|
305
310
|
pendingRange = range;
|
|
311
|
+
pendingCopyText = fullText;
|
|
306
312
|
pendingSelection = {
|
|
307
313
|
text: text,
|
|
308
314
|
startContainerPath: getNodePath(range.startContainer),
|
|
@@ -320,6 +326,13 @@
|
|
|
320
326
|
targetKey: (extras && extras.targetKey) || undefined,
|
|
321
327
|
targetLabel: (extras && extras.targetLabel) || undefined,
|
|
322
328
|
context: (extras && extras.context) || undefined,
|
|
329
|
+
// This bridge answers rich-copy-request for a text selection (the
|
|
330
|
+
// toolbar's rich-text Copy). Optional and additive: an older bridge
|
|
331
|
+
// omits it and Copy stays plain text, so no protocol version bump.
|
|
332
|
+
richCopy: (extras && extras.pinpoint) ? undefined : true,
|
|
333
|
+
// The quote above was cut to MAX_SELECTION_TEXT: a plain-text copy made
|
|
334
|
+
// from it alone (no answer to rich-copy-request) says so.
|
|
335
|
+
textCut: text.length < fullText.length ? true : undefined,
|
|
323
336
|
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
|
|
324
337
|
});
|
|
325
338
|
renderAnnotationOverlay(); // draft selection highlight (overlay-projected)
|
|
@@ -368,6 +381,46 @@
|
|
|
368
381
|
if (!e.data || !e.data.type) return;
|
|
369
382
|
var type = e.data.type;
|
|
370
383
|
|
|
384
|
+
// Copy (rich text) was pressed in the parent: build the pending text
|
|
385
|
+
// range's markup now (never on selection). A throw or an oversized range
|
|
386
|
+
// answers without markup, so the parent copies plain text.
|
|
387
|
+
if (type === PREFIX + 'rich-copy-request') {
|
|
388
|
+
// Text and markup are each capped at 256 KiB: past it the text is cut
|
|
389
|
+
// (never at half a surrogate pair) and marked, and the markup is not
|
|
390
|
+
// built at all (too large for rich text).
|
|
391
|
+
var copyHtml = '';
|
|
392
|
+
var copyText = '';
|
|
393
|
+
var copyTruncated = false;
|
|
394
|
+
var copyTooLarge = false;
|
|
395
|
+
try {
|
|
396
|
+
if (pendingRange && pendingSelection && pendingSelection.startContainerPath) {
|
|
397
|
+
copyText = pendingCopyText || pendingRange.toString();
|
|
398
|
+
if (copyText.length > 262144) {
|
|
399
|
+
var copyCut = 262144;
|
|
400
|
+
var copyLast = copyText.charCodeAt(copyCut - 1);
|
|
401
|
+
if (copyLast >= 0xd800 && copyLast <= 0xdbff) copyCut -= 1;
|
|
402
|
+
copyText = copyText.slice(0, copyCut);
|
|
403
|
+
copyTruncated = true;
|
|
404
|
+
copyTooLarge = true;
|
|
405
|
+
} else {
|
|
406
|
+
copyHtml = htmlForRange(pendingRange);
|
|
407
|
+
if (copyHtml.length > 262144) copyTooLarge = true;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
copyHtml = '';
|
|
412
|
+
}
|
|
413
|
+
postToParent({
|
|
414
|
+
type: PREFIX + 'rich-copy',
|
|
415
|
+
id: typeof e.data.id === 'string' ? e.data.id : '',
|
|
416
|
+
html: copyTooLarge ? '' : copyHtml,
|
|
417
|
+
text: copyText,
|
|
418
|
+
truncated: copyTruncated,
|
|
419
|
+
tooLarge: copyTooLarge
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
371
424
|
if (type === PREFIX + 'create-mark') {
|
|
372
425
|
var id = e.data.id;
|
|
373
426
|
var annType = e.data.annotationType || 'comment';
|
|
@@ -1085,6 +1138,10 @@
|
|
|
1085
1138
|
return pinpointBoxEl;
|
|
1086
1139
|
}
|
|
1087
1140
|
function positionPinpointBox(el) {
|
|
1141
|
+
if (pendingPinHidden(el)) {
|
|
1142
|
+
if (pinpointBoxEl) pinpointBoxEl.style.display = 'none';
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1088
1145
|
var r = el.getBoundingClientRect();
|
|
1089
1146
|
var box = getPinpointBoxEl();
|
|
1090
1147
|
box.style.display = 'block';
|
|
@@ -1121,6 +1178,14 @@
|
|
|
1121
1178
|
// changes); shared by mousemove and the scroll/resize reconcile pass.
|
|
1122
1179
|
// Per-event hit-testing only — never builds the semantic graph.
|
|
1123
1180
|
function updatePinpointHover(x, y, fallbackNode) {
|
|
1181
|
+
// A link a plain click follows shows no pin outline unless Cmd/Ctrl is
|
|
1182
|
+
// held: the outline always matches what a click there will do.
|
|
1183
|
+
if (!pinModifierHeld && !pendingPinEl && (followableLinkAt(fallbackNode) || followableLinkAt(deepElementFromPoint(x, y)))) {
|
|
1184
|
+
pinpointHover = null;
|
|
1185
|
+
hidePinpointBox();
|
|
1186
|
+
hidePinpointLabel();
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1124
1189
|
var el = pinpointLeafAt(x, y, fallbackNode);
|
|
1125
1190
|
// The 25px marker bubble owns clicks over it (they select its comment),
|
|
1126
1191
|
// so hover must advertise the MARKER's own identity — no annotate box,
|
|
@@ -1151,9 +1216,36 @@
|
|
|
1151
1216
|
positionPinpointLabel(el, pinpointHoverLabel(el));
|
|
1152
1217
|
}
|
|
1153
1218
|
|
|
1219
|
+
// Cmd or Ctrl held (the pin modifier): a link under the pointer shows the
|
|
1220
|
+
// pin outline and the crosshair instead of the follow pointer. Read from
|
|
1221
|
+
// the key itself (as the token-hover gate does) and from every pointer
|
|
1222
|
+
// move, so a key pressed with focus outside this frame still counts.
|
|
1223
|
+
var pinModifierHeld = false;
|
|
1224
|
+
function setPinModifier(held) {
|
|
1225
|
+
if (held === pinModifierHeld) return;
|
|
1226
|
+
pinModifierHeld = held;
|
|
1227
|
+
if (document.body) {
|
|
1228
|
+
if (held) document.body.setAttribute('data-plannotator-pin-modifier', '');
|
|
1229
|
+
else document.body.removeAttribute('data-plannotator-pin-modifier');
|
|
1230
|
+
}
|
|
1231
|
+
if (annotateModeActive && currentInputMethod === 'pinpoint' && lastPointer && !(vimEnabled && vimPhase !== 'inactive')) {
|
|
1232
|
+
updatePinpointHover(lastPointer.x, lastPointer.y, lastPointerNode || pinpointHover);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
document.addEventListener('keydown', function(e) {
|
|
1236
|
+
if (e.key === 'Meta' || e.key === 'Control') setPinModifier(true);
|
|
1237
|
+
}, true);
|
|
1238
|
+
document.addEventListener('keyup', function(e) {
|
|
1239
|
+
if (e.key === 'Meta' || e.key === 'Control') setPinModifier(e.metaKey || e.ctrlKey);
|
|
1240
|
+
}, true);
|
|
1241
|
+
window.addEventListener('blur', function() { setPinModifier(false); });
|
|
1242
|
+
|
|
1154
1243
|
var lastPointer = null;
|
|
1244
|
+
var lastPointerNode = null;
|
|
1155
1245
|
document.addEventListener('mousemove', function(e) {
|
|
1156
1246
|
lastPointer = { x: e.clientX, y: e.clientY };
|
|
1247
|
+
lastPointerNode = e.target && e.target.nodeType === 1 ? e.target : null;
|
|
1248
|
+
if (!!(e.metaKey || e.ctrlKey) !== pinModifierHeld) setPinModifier(!!(e.metaKey || e.ctrlKey));
|
|
1157
1249
|
updateDragYield(e);
|
|
1158
1250
|
if (!annotateModeActive) return; // Interact mode: no hover affordances
|
|
1159
1251
|
if (currentInputMethod !== 'pinpoint') {
|
|
@@ -1203,6 +1295,8 @@
|
|
|
1203
1295
|
if (annotateModeActive && currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
|
|
1204
1296
|
setHoverHighlight(committedHighlightIdAtCached(lastPointer.x, lastPointer.y));
|
|
1205
1297
|
}
|
|
1298
|
+
// Before the boxes: it may re-find a replaced target they then follow.
|
|
1299
|
+
reconcilePendingPinVisibility();
|
|
1206
1300
|
positionMultiTargetBoxes();
|
|
1207
1301
|
if (pendingPinEl && pendingPinEl.isConnected) {
|
|
1208
1302
|
positionPinpointBox(pendingPinEl);
|
|
@@ -3195,7 +3289,91 @@
|
|
|
3195
3289
|
}
|
|
3196
3290
|
}
|
|
3197
3291
|
|
|
3292
|
+
// A draft's composer hides with its target and keeps the draft: a slide the
|
|
3293
|
+
// page hid (display:none, visibility:hidden, no box) is not a scroll-out.
|
|
3294
|
+
// Offscreen targets stay visible to support roaming multi-select. A target
|
|
3295
|
+
// the page REPLACED with a new element (a framework re-render, a live-app
|
|
3296
|
+
// route change) is found again through its anchor, the way restore re-finds
|
|
3297
|
+
// committed targets; one that cannot be found keeps the composer visible,
|
|
3298
|
+
// so a draft is never left where nobody can reach it.
|
|
3299
|
+
var pendingPinVisible = true;
|
|
3300
|
+
var pendingPinSearchGen = 0;
|
|
3301
|
+
function pendingPinHidden(el) {
|
|
3302
|
+
if (!el || !el.isConnected) return false;
|
|
3303
|
+
if (targetStyleHidden(el)) return true;
|
|
3304
|
+
var rect = el.getBoundingClientRect();
|
|
3305
|
+
return layoutActive() && !rect.width && !rect.height;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function refindPendingTarget(anchor, taken) {
|
|
3309
|
+
var found = resolveAnchorElement(anchor);
|
|
3310
|
+
if (!found || isViewerOverlayNode(found)) return null;
|
|
3311
|
+
for (var i = 0; i < taken.length; i++) if (taken[i] === found) return null;
|
|
3312
|
+
return found;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// Re-find detached draft targets, once per DOM generation each. Returns
|
|
3316
|
+
// whether the primary moved to a new element.
|
|
3317
|
+
function refreshPendingTargets() {
|
|
3318
|
+
var taken = [pendingPinEl];
|
|
3319
|
+
for (var t = 0; t < pendingMultiTargets.length; t++) taken.push(pendingMultiTargets[t].el);
|
|
3320
|
+
var moved = false;
|
|
3321
|
+
if (pendingPinEl && !pendingPinEl.isConnected && pendingPinAnchor && pendingPinSearchGen !== domGeneration) {
|
|
3322
|
+
pendingPinSearchGen = domGeneration;
|
|
3323
|
+
var found = refindPendingTarget(pendingPinAnchor, taken);
|
|
3324
|
+
if (found) {
|
|
3325
|
+
pendingPinEl = found;
|
|
3326
|
+
// The text range belonged to the replaced element: commit as an
|
|
3327
|
+
// element pin on the new one (as a promoted primary does).
|
|
3328
|
+
if (pendingRange && !rangeAlive(pendingRange)) pendingRange = null;
|
|
3329
|
+
if (pendingSelection && pendingSelection.startContainerPath) {
|
|
3330
|
+
pendingSelection = { element: true, text: pendingSelection.text };
|
|
3331
|
+
}
|
|
3332
|
+
moved = true;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
for (var i = 0; i < pendingMultiTargets.length; i++) {
|
|
3336
|
+
var entry = pendingMultiTargets[i];
|
|
3337
|
+
if (!entry.el || entry.el.isConnected || !entry.anchor || entry.searchGen === domGeneration) continue;
|
|
3338
|
+
entry.searchGen = domGeneration;
|
|
3339
|
+
var next = refindPendingTarget(entry.anchor, taken);
|
|
3340
|
+
if (next) { entry.el = next; taken.push(next); }
|
|
3341
|
+
}
|
|
3342
|
+
return moved;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// Re-anchor the parent's composer on the pin (it cannot see the page).
|
|
3346
|
+
function postPendingPinRect() {
|
|
3347
|
+
if (!pendingSelection || !pendingPinEl || !pendingPinEl.isConnected) return;
|
|
3348
|
+
var tracked = pendingRange && rangeAlive(pendingRange) ? pendingRange : pendingPinEl;
|
|
3349
|
+
var r = tracked.getBoundingClientRect();
|
|
3350
|
+
postToParent({
|
|
3351
|
+
type: PREFIX + 'selection-rect',
|
|
3352
|
+
rect: { top: r.top, left: r.left, width: r.width, height: r.height }
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
function reconcilePendingPinVisibility() {
|
|
3357
|
+
if (!pendingPinEl) return;
|
|
3358
|
+
var moved = refreshPendingTargets();
|
|
3359
|
+
var visible = !pendingPinHidden(pendingPinEl);
|
|
3360
|
+
for (var i = 0; !visible && i < pendingMultiTargets.length; i++) {
|
|
3361
|
+
visible = !pendingPinHidden(pendingMultiTargets[i].el);
|
|
3362
|
+
}
|
|
3363
|
+
// A detached primary has no geometry: never leave its outline behind.
|
|
3364
|
+
if (!pendingPinEl.isConnected && pinpointBoxEl) pinpointBoxEl.style.display = 'none';
|
|
3365
|
+
var shown = visible && !pendingPinVisible;
|
|
3366
|
+
if (visible !== pendingPinVisible) {
|
|
3367
|
+
pendingPinVisible = visible;
|
|
3368
|
+
postToParent({ type: PREFIX + 'pin-visibility', visible: visible });
|
|
3369
|
+
if (!visible && pinpointBoxEl) pinpointBoxEl.style.display = 'none';
|
|
3370
|
+
}
|
|
3371
|
+
if (visible && (moved || shown)) postPendingPinRect();
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3198
3374
|
function clearPendingPin() {
|
|
3375
|
+
pendingPinVisible = true;
|
|
3376
|
+
pendingPinSearchGen = 0;
|
|
3199
3377
|
pendingPinEl = null;
|
|
3200
3378
|
pendingPinContext = null;
|
|
3201
3379
|
pendingPinAnchor = null;
|
|
@@ -3251,6 +3429,7 @@
|
|
|
3251
3429
|
}
|
|
3252
3430
|
|
|
3253
3431
|
function positionBoxToEl(box, el) {
|
|
3432
|
+
if (pendingPinHidden(el)) { box.style.display = 'none'; return; }
|
|
3254
3433
|
var r = el.getBoundingClientRect();
|
|
3255
3434
|
box.style.display = 'block';
|
|
3256
3435
|
box.style.left = r.left + 'px';
|
|
@@ -3382,6 +3561,7 @@
|
|
|
3382
3561
|
*/
|
|
3383
3562
|
function removeMultiTargetByKey(key, echo) {
|
|
3384
3563
|
if (!key) return;
|
|
3564
|
+
schedulePinpointReconcile();
|
|
3385
3565
|
if (key === pendingPinKey) {
|
|
3386
3566
|
if (!pendingMultiTargets.length) {
|
|
3387
3567
|
// Last target gone — the draft is cancelled.
|
|
@@ -3398,6 +3578,7 @@
|
|
|
3398
3578
|
var next = pendingMultiTargets.shift();
|
|
3399
3579
|
destroyMultiTargetBox(next.box);
|
|
3400
3580
|
pendingPinEl = next.el;
|
|
3581
|
+
pendingPinSearchGen = 0;
|
|
3401
3582
|
pendingPinAnchor = next.anchor;
|
|
3402
3583
|
pendingPinKey = next.key;
|
|
3403
3584
|
pendingPinLabel = next.label;
|
|
@@ -3465,6 +3646,7 @@
|
|
|
3465
3646
|
var key = makeTargetKey();
|
|
3466
3647
|
var box = createMultiTargetBox(el);
|
|
3467
3648
|
pendingMultiTargets.push({ key: key, el: el, anchor: anchor, label: label, text: text, point: point, context: context, box: box });
|
|
3649
|
+
schedulePinpointReconcile();
|
|
3468
3650
|
postToParent({
|
|
3469
3651
|
type: PREFIX + 'multi-target-added',
|
|
3470
3652
|
key: key,
|
|
@@ -3599,6 +3781,8 @@
|
|
|
3599
3781
|
multiSelectArmed = false;
|
|
3600
3782
|
multiSelectMax = MAX_MULTI_TARGETS;
|
|
3601
3783
|
pendingPinEl = el;
|
|
3784
|
+
pendingPinVisible = true;
|
|
3785
|
+
pendingPinSearchGen = 0;
|
|
3602
3786
|
pendingPinAnchor = buildElementAnchor(el);
|
|
3603
3787
|
pendingPinKey = makeTargetKey();
|
|
3604
3788
|
pendingPinLabel = pinpointHoverLabel(el);
|
|
@@ -3663,6 +3847,230 @@
|
|
|
3663
3847
|
return true;
|
|
3664
3848
|
}
|
|
3665
3849
|
|
|
3850
|
+
// --- Touch: a tap on a link follows it, press-and-hold pins it ---
|
|
3851
|
+
// The owner's amendment to html-link-click (2026-10-20): on a phone there is
|
|
3852
|
+
// no Cmd/Ctrl, so while armed a TAP on a link follows it (the click handlers
|
|
3853
|
+
// below take it as a plain click) and a PRESS-AND-HOLD of HOLD_PIN_MS opens
|
|
3854
|
+
// a comment on it, pinned exactly as a Cmd-click pins it. Every surface
|
|
3855
|
+
// alike: srcdoc pages (annotate, plan review's HTML, the Inbox) and live apps.
|
|
3856
|
+
// A finger that moves past HOLD_SLOP_PX (a scroll), a second finger, or a
|
|
3857
|
+
// scroll cancels the hold. The release of a hold that pinned never follows
|
|
3858
|
+
// the link and never reaches the page: its pointerup and touchend are
|
|
3859
|
+
// stopped at the window (registered before a live app's own listeners),
|
|
3860
|
+
// touchend is cancelled so no click is synthesized, and a click that still
|
|
3861
|
+
// arrives is swallowed once. That swallow ends at the next touch, mouse
|
|
3862
|
+
// press or key, so it never eats an unrelated click.
|
|
3863
|
+
// While a hold is in progress, link text is not selectable (the CSS above),
|
|
3864
|
+
// so the platform's long-press starts no native selection under the finger;
|
|
3865
|
+
// Android's contextmenu, its long-press signal, fires the hold at once.
|
|
3866
|
+
var HOLD_PIN_MS = 500;
|
|
3867
|
+
var HOLD_SLOP_PX = 10;
|
|
3868
|
+
var HOLD_SWALLOW_MAX_MS = 10000; // a release never seen (an app stopped touchend)
|
|
3869
|
+
var linkHold = null; // { timer, x, y, id, target }
|
|
3870
|
+
// After a hold pinned: { pointerId, released, until } until its release's
|
|
3871
|
+
// click (if any) is swallowed or the next gesture starts.
|
|
3872
|
+
var heldGesture = null;
|
|
3873
|
+
var lastTouchPointerId = null;
|
|
3874
|
+
function holdArmed() {
|
|
3875
|
+
return annotateModeActive && currentInputMethod === 'pinpoint' && !(vimEnabled && vimPhase !== 'inactive');
|
|
3876
|
+
}
|
|
3877
|
+
function setLinkHoldStyle(on) {
|
|
3878
|
+
if (!document.body) return;
|
|
3879
|
+
if (on) document.body.setAttribute('data-plannotator-link-hold', '');
|
|
3880
|
+
else document.body.removeAttribute('data-plannotator-link-hold');
|
|
3881
|
+
}
|
|
3882
|
+
function cancelLinkHold() {
|
|
3883
|
+
if (!linkHold) return;
|
|
3884
|
+
clearTimeout(linkHold.timer);
|
|
3885
|
+
linkHold = null;
|
|
3886
|
+
setLinkHoldStyle(false);
|
|
3887
|
+
}
|
|
3888
|
+
function fireLinkHold() {
|
|
3889
|
+
var hold = linkHold;
|
|
3890
|
+
if (!hold) return false;
|
|
3891
|
+
clearTimeout(hold.timer);
|
|
3892
|
+
linkHold = null;
|
|
3893
|
+
if (!holdArmed() || !hold.target.isConnected) {
|
|
3894
|
+
setLinkHoldStyle(false);
|
|
3895
|
+
return false;
|
|
3896
|
+
}
|
|
3897
|
+
var el = resolvePinpointTargetAt(hold.x, hold.y, hold.target);
|
|
3898
|
+
if (!el) {
|
|
3899
|
+
setLinkHoldStyle(false);
|
|
3900
|
+
return false;
|
|
3901
|
+
}
|
|
3902
|
+
heldGesture = { pointerId: lastTouchPointerId, released: false, pointerUpSeen: false, pointerEnded: false, touchEnded: false, target: hold.target, touchId: hold.id, x: hold.x, y: hold.y, until: Date.now() + HOLD_SWALLOW_MAX_MS };
|
|
3903
|
+
// The same pin a Cmd-click makes: the link's own words as a text range.
|
|
3904
|
+
// Selection is back on for that, then off again until the finger lifts.
|
|
3905
|
+
setLinkHoldStyle(false);
|
|
3906
|
+
try { window.getSelection().removeAllRanges(); } catch (ex) {}
|
|
3907
|
+
annotateElement(el, undefined, true, { x: hold.x, y: hold.y });
|
|
3908
|
+
setLinkHoldStyle(true);
|
|
3909
|
+
return true;
|
|
3910
|
+
}
|
|
3911
|
+
function endHeldGesture() {
|
|
3912
|
+
heldGesture = null;
|
|
3913
|
+
}
|
|
3914
|
+
function touchById(list, id) {
|
|
3915
|
+
for (var i = 0; list && i < list.length; i++) if (list[i].identifier === id) return list[i];
|
|
3916
|
+
return null;
|
|
3917
|
+
}
|
|
3918
|
+
// The release of a hold is stopped, so the page never sees it end: a press
|
|
3919
|
+
// library (React Aria's usePress, Motion's whileTap, dnd-kit) would keep the
|
|
3920
|
+
// press open. In its place the page gets a pointercancel and a touchcancel
|
|
3921
|
+
// on the element the finger went down on, which end a press without
|
|
3922
|
+
// activating it. The bridge's own listeners ignore those two.
|
|
3923
|
+
var bridgeSynthetic = typeof WeakSet === 'function' ? new WeakSet() : null;
|
|
3924
|
+
function isBridgeSynthetic(e) {
|
|
3925
|
+
return !!(bridgeSynthetic && bridgeSynthetic.has(e));
|
|
3926
|
+
}
|
|
3927
|
+
function dispatchSynthetic(target, event) {
|
|
3928
|
+
if (bridgeSynthetic) bridgeSynthetic.add(event);
|
|
3929
|
+
try { target.dispatchEvent(event); } catch (ex) {}
|
|
3930
|
+
}
|
|
3931
|
+
function sendPointerCancel(g, pointerId) {
|
|
3932
|
+
if (g.pointerEnded || pointerId === null || pointerId === undefined) return;
|
|
3933
|
+
g.pointerEnded = true;
|
|
3934
|
+
var target = g.target && g.target.isConnected ? g.target : document.body;
|
|
3935
|
+
if (!target) return;
|
|
3936
|
+
var init = { bubbles: true, cancelable: false, composed: true, pointerId: pointerId, pointerType: 'touch', isPrimary: true, clientX: g.x, clientY: g.y };
|
|
3937
|
+
var event = null;
|
|
3938
|
+
try { if (typeof PointerEvent === 'function') event = new PointerEvent('pointercancel', init); } catch (ex) {}
|
|
3939
|
+
if (!event) {
|
|
3940
|
+
event = new Event('pointercancel', { bubbles: true, cancelable: false, composed: true });
|
|
3941
|
+
try {
|
|
3942
|
+
Object.defineProperty(event, 'pointerId', { value: pointerId });
|
|
3943
|
+
Object.defineProperty(event, 'pointerType', { value: 'touch' });
|
|
3944
|
+
Object.defineProperty(event, 'isPrimary', { value: true });
|
|
3945
|
+
} catch (ex) {}
|
|
3946
|
+
}
|
|
3947
|
+
dispatchSynthetic(target, event);
|
|
3948
|
+
}
|
|
3949
|
+
function sendTouchCancel(g) {
|
|
3950
|
+
if (g.touchEnded) return;
|
|
3951
|
+
g.touchEnded = true;
|
|
3952
|
+
var target = g.target && g.target.isConnected ? g.target : document.body;
|
|
3953
|
+
if (!target) return;
|
|
3954
|
+
var event = null;
|
|
3955
|
+
try {
|
|
3956
|
+
if (typeof TouchEvent === 'function' && typeof Touch === 'function') {
|
|
3957
|
+
var t = new Touch({ identifier: g.touchId, target: target, clientX: g.x, clientY: g.y });
|
|
3958
|
+
event = new TouchEvent('touchcancel', { bubbles: true, cancelable: false, composed: true, touches: [], targetTouches: [], changedTouches: [t] });
|
|
3959
|
+
}
|
|
3960
|
+
} catch (ex) { event = null; }
|
|
3961
|
+
if (!event) {
|
|
3962
|
+
event = new Event('touchcancel', { bubbles: true, cancelable: false, composed: true });
|
|
3963
|
+
var point = { identifier: g.touchId, target: target, clientX: g.x, clientY: g.y };
|
|
3964
|
+
try {
|
|
3965
|
+
Object.defineProperty(event, 'touches', { value: [] });
|
|
3966
|
+
Object.defineProperty(event, 'targetTouches', { value: [] });
|
|
3967
|
+
Object.defineProperty(event, 'changedTouches', { value: [point] });
|
|
3968
|
+
} catch (ex) {}
|
|
3969
|
+
}
|
|
3970
|
+
dispatchSynthetic(target, event);
|
|
3971
|
+
}
|
|
3972
|
+
// Pointer events come before touch events; the touch's pointerId names the
|
|
3973
|
+
// release's pointerup.
|
|
3974
|
+
window.addEventListener('pointerdown', function(e) {
|
|
3975
|
+
if (isBridgeSynthetic(e)) return;
|
|
3976
|
+
if (e.pointerType === 'touch') {
|
|
3977
|
+
lastTouchPointerId = e.pointerId;
|
|
3978
|
+
if (heldGesture && heldGesture.released) endHeldGesture();
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
// A mouse or pen press: whatever click comes next is its own.
|
|
3982
|
+
endHeldGesture();
|
|
3983
|
+
}, true);
|
|
3984
|
+
window.addEventListener('keydown', function() { if (heldGesture && heldGesture.released) endHeldGesture(); }, true);
|
|
3985
|
+
function heldPointer(e) {
|
|
3986
|
+
if (!heldGesture || e.pointerType !== 'touch') return false;
|
|
3987
|
+
return heldGesture.pointerId === null || e.pointerId === heldGesture.pointerId;
|
|
3988
|
+
}
|
|
3989
|
+
window.addEventListener('pointerup', function(e) {
|
|
3990
|
+
if (isBridgeSynthetic(e) || !heldPointer(e) || heldGesture.pointerUpSeen) return;
|
|
3991
|
+
heldGesture.pointerUpSeen = true;
|
|
3992
|
+
e.stopImmediatePropagation();
|
|
3993
|
+
sendPointerCancel(heldGesture, e.pointerId);
|
|
3994
|
+
}, true);
|
|
3995
|
+
// The browser ended the held pointer itself (it took the touch): nothing to stand in for.
|
|
3996
|
+
window.addEventListener('pointercancel', function(e) {
|
|
3997
|
+
if (isBridgeSynthetic(e) || !heldPointer(e)) return;
|
|
3998
|
+
heldGesture.pointerUpSeen = true;
|
|
3999
|
+
heldGesture.pointerEnded = true;
|
|
4000
|
+
}, true);
|
|
4001
|
+
window.addEventListener('touchend', function(e) {
|
|
4002
|
+
if (isBridgeSynthetic(e)) return;
|
|
4003
|
+
if (heldGesture && !heldGesture.released) {
|
|
4004
|
+
// The release of the hold that pinned: no click, no navigation, and
|
|
4005
|
+
// the page's own touchend never runs on it; it gets cancels instead.
|
|
4006
|
+
if (e.cancelable) e.preventDefault();
|
|
4007
|
+
e.stopImmediatePropagation();
|
|
4008
|
+
var g = heldGesture;
|
|
4009
|
+
g.released = true;
|
|
4010
|
+
g.until = Date.now() + 800;
|
|
4011
|
+
setLinkHoldStyle(false);
|
|
4012
|
+
// Engines that send touchend before pointerup: the pointer ends here
|
|
4013
|
+
// (its own pointerup, when it comes, is still stopped).
|
|
4014
|
+
sendPointerCancel(g, g.pointerId);
|
|
4015
|
+
sendTouchCancel(g);
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
cancelLinkHold(); // a tap: the click that follows it follows the link
|
|
4019
|
+
}, { capture: true, passive: false });
|
|
4020
|
+
window.addEventListener('touchcancel', function(e) {
|
|
4021
|
+
if (isBridgeSynthetic(e)) return;
|
|
4022
|
+
cancelLinkHold();
|
|
4023
|
+
setLinkHoldStyle(false);
|
|
4024
|
+
if (heldGesture) {
|
|
4025
|
+
heldGesture.released = true;
|
|
4026
|
+
heldGesture.touchEnded = true;
|
|
4027
|
+
heldGesture.until = Date.now() + 800;
|
|
4028
|
+
}
|
|
4029
|
+
}, true);
|
|
4030
|
+
document.addEventListener('touchstart', function(e) {
|
|
4031
|
+
if (document.body && !document.body.hasAttribute('data-plannotator-touch')) {
|
|
4032
|
+
document.body.setAttribute('data-plannotator-touch', '');
|
|
4033
|
+
}
|
|
4034
|
+
cancelLinkHold();
|
|
4035
|
+
// A new touch: whatever click comes next is its own, even if the last
|
|
4036
|
+
// hold's release was never seen.
|
|
4037
|
+
endHeldGesture();
|
|
4038
|
+
setLinkHoldStyle(false);
|
|
4039
|
+
if (!holdArmed() || !e.touches || e.touches.length !== 1) return;
|
|
4040
|
+
if (isViewerOverlayNode(e.target) || !followableLinkAt(e.target)) return;
|
|
4041
|
+
var t = e.touches[0];
|
|
4042
|
+
linkHold = { x: t.clientX, y: t.clientY, id: t.identifier, target: e.target, timer: 0 };
|
|
4043
|
+
linkHold.timer = setTimeout(fireLinkHold, HOLD_PIN_MS);
|
|
4044
|
+
setLinkHoldStyle(true);
|
|
4045
|
+
}, { capture: true, passive: true });
|
|
4046
|
+
document.addEventListener('touchmove', function(e) {
|
|
4047
|
+
if (!linkHold) return;
|
|
4048
|
+
var t = touchById(e.touches, linkHold.id);
|
|
4049
|
+
if (!t || e.touches.length !== 1 || Math.abs(t.clientX - linkHold.x) > HOLD_SLOP_PX || Math.abs(t.clientY - linkHold.y) > HOLD_SLOP_PX) {
|
|
4050
|
+
cancelLinkHold();
|
|
4051
|
+
}
|
|
4052
|
+
}, { capture: true, passive: true });
|
|
4053
|
+
window.addEventListener('scroll', cancelLinkHold, true);
|
|
4054
|
+
document.addEventListener('contextmenu', function(e) {
|
|
4055
|
+
if (linkHold) {
|
|
4056
|
+
e.preventDefault();
|
|
4057
|
+
fireLinkHold();
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
if (heldGesture && Date.now() < heldGesture.until) e.preventDefault();
|
|
4061
|
+
}, true);
|
|
4062
|
+
// On the window, before every other click handler of the bridge and a live
|
|
4063
|
+
// app's own, so the release of a hold reaches neither the link handler nor
|
|
4064
|
+
// the pinpoint one, nor the page (a live app would navigate).
|
|
4065
|
+
window.addEventListener('click', function(e) {
|
|
4066
|
+
if (!heldGesture) return;
|
|
4067
|
+
var swallow = Date.now() < heldGesture.until;
|
|
4068
|
+
endHeldGesture();
|
|
4069
|
+
if (!swallow) return;
|
|
4070
|
+
e.preventDefault();
|
|
4071
|
+
e.stopImmediatePropagation();
|
|
4072
|
+
}, true);
|
|
4073
|
+
|
|
3666
4074
|
// --- Local-site link navigation (srcdoc sessions only) ---
|
|
3667
4075
|
// A srcdoc document has no URL of its own: its base URL is the PARENT page's,
|
|
3668
4076
|
// which is the Plannotator server. So a plain link to 02-detail.html resolves
|
|
@@ -3674,8 +4082,14 @@
|
|
|
3674
4082
|
// The frame therefore never navigates itself. In-page fragments scroll here;
|
|
3675
4083
|
// everything else is handed to the parent, which owns resolution against the
|
|
3676
4084
|
// current document's directory and is the trust boundary for the href.
|
|
3677
|
-
// Registered BEFORE the pinpoint handler and never stopping propagation
|
|
3678
|
-
//
|
|
4085
|
+
// Registered BEFORE the pinpoint handler and never stopping propagation.
|
|
4086
|
+
//
|
|
4087
|
+
// One rule while armed (the owner's ruling, html-link-click, 2026-10-20):
|
|
4088
|
+
// a plain click on a link FOLLOWS it, exactly as in Interact; a click with
|
|
4089
|
+
// the platform modifier (Cmd on macOS, Ctrl elsewhere: either is taken)
|
|
4090
|
+
// PINS it for a comment through the pinpoint handler below, as does a
|
|
4091
|
+
// shift-click that adds it to an open multi-select draft. A drag that ended
|
|
4092
|
+
// on the link owns its click: it neither follows nor pins.
|
|
3679
4093
|
//
|
|
3680
4094
|
// Live app sessions are excluded outright: they navigate a real origin
|
|
3681
4095
|
// through the proxy, which is the whole point of that surface.
|
|
@@ -3698,6 +4112,25 @@
|
|
|
3698
4112
|
return true;
|
|
3699
4113
|
}
|
|
3700
4114
|
|
|
4115
|
+
// While armed, whether a click on a link pins it instead of following it:
|
|
4116
|
+
// the platform modifier (Cmd or Ctrl), a shift-click adding to an open
|
|
4117
|
+
// multi-select draft, or the trailing click of a drag that selected text.
|
|
4118
|
+
function armedClickPins(e) {
|
|
4119
|
+
if (e.metaKey || e.ctrlKey || dragEndedClick) return true;
|
|
4120
|
+
return !!(e.shiftKey && pendingPinEl && multiSelectArmed && pendingSelection);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// A link with a real destination: one a plain click follows while armed.
|
|
4124
|
+
// No href, "", "#" and javascript: go nowhere, so a click on them pins
|
|
4125
|
+
// the element as on any other (agents' mocks use <a href="#"> for buttons).
|
|
4126
|
+
function isFollowableHref(raw) {
|
|
4127
|
+
return !!raw && raw !== '#' && !/^javascript:/i.test(raw);
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
function followableLinkAt(node) {
|
|
4131
|
+
return !!node && isFollowableHref(navigableLinkHref(node));
|
|
4132
|
+
}
|
|
4133
|
+
|
|
3701
4134
|
function navigableLinkHref(node) {
|
|
3702
4135
|
var el = node && node.nodeType === 1 ? node : node && node.parentElement;
|
|
3703
4136
|
if (!el || !el.closest) return '';
|
|
@@ -3717,26 +4150,30 @@
|
|
|
3717
4150
|
if (!raw) return;
|
|
3718
4151
|
// The page's own scripting, not a navigation: leave it alone.
|
|
3719
4152
|
if (/^javascript:/i.test(raw)) return;
|
|
4153
|
+
// Armed and pinning (a link that goes nowhere, the modifier, or a
|
|
4154
|
+
// drag's trailing click): the frame never navigates, and the pinpoint
|
|
4155
|
+
// handler below takes the click.
|
|
4156
|
+
if (annotateModeActive && currentInputMethod === 'pinpoint' && (!isFollowableHref(raw) || armedClickPins(e))) {
|
|
4157
|
+
e.preventDefault();
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
3720
4160
|
if (raw.charAt(0) === '#') {
|
|
3721
4161
|
e.preventDefault();
|
|
3722
4162
|
scrollToLocalFragment(raw.slice(1));
|
|
3723
4163
|
return;
|
|
3724
4164
|
}
|
|
3725
4165
|
e.preventDefault();
|
|
4166
|
+
// Only a person's click follows a link: a page's own script calling
|
|
4167
|
+
// a.click() never navigates the viewer or pushes the host's history.
|
|
4168
|
+
if (!e.isTrusted) return;
|
|
3726
4169
|
// A native host decides links itself: a person's tap opens the link as a
|
|
3727
4170
|
// new window, which the host's web view hands to its own delegate (the
|
|
3728
4171
|
// frame never navigates; the host page's CSP would refuse it anyway).
|
|
3729
4172
|
if (hostNavigates) {
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
try { window.open(target && target.href ? target.href : raw, '_blank', 'noopener'); } catch (ex) {}
|
|
3733
|
-
}
|
|
4173
|
+
var target = (e.target && e.target.closest) ? e.target.closest('a,area') : null;
|
|
4174
|
+
try { window.open(target && target.href ? target.href : raw, '_blank', 'noopener'); } catch (ex) {}
|
|
3734
4175
|
return;
|
|
3735
4176
|
}
|
|
3736
|
-
// Armed pinpoint: the click belongs to annotation, and the capture-phase
|
|
3737
|
-
// pinpoint handler below is about to pin this element. Navigation is
|
|
3738
|
-
// already suppressed above, which is all this surface owes the click.
|
|
3739
|
-
if (annotateModeActive && currentInputMethod === 'pinpoint') return;
|
|
3740
4177
|
postToParent({ type: PREFIX + 'link-click', href: raw.slice(0, 2048) });
|
|
3741
4178
|
}, true);
|
|
3742
4179
|
}
|
|
@@ -3755,7 +4192,12 @@
|
|
|
3755
4192
|
// and mousedown clears a prior pin's leftover selection, so stale
|
|
3756
4193
|
// selection state never blocks the next plain re-pin click. One-shot:
|
|
3757
4194
|
// only the drag's own trailing click is suppressed.
|
|
3758
|
-
if (dragEndedClick) {
|
|
4195
|
+
if (dragEndedClick) {
|
|
4196
|
+
dragEndedClick = false;
|
|
4197
|
+
// Never a navigation either (a live app's link would otherwise follow).
|
|
4198
|
+
if (navigableLinkHref(e.target)) e.preventDefault();
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
3759
4201
|
// Shift-click while an ARMED pinpoint draft is open: toggle the element
|
|
3760
4202
|
// in/out of the SAME draft comment instead of replacing the selection.
|
|
3761
4203
|
// Unarmed drafts (modes the parent does not mirror, e.g. quickLabel)
|
|
@@ -3769,6 +4211,9 @@
|
|
|
3769
4211
|
if (multiEl) toggleMultiTarget(multiEl, { x: e.clientX, y: e.clientY });
|
|
3770
4212
|
return;
|
|
3771
4213
|
}
|
|
4214
|
+
// A plain click on a link follows it (the link handler above posted it;
|
|
4215
|
+
// in a live app the browser navigates for real). The modifier pins it.
|
|
4216
|
+
if (isFollowableHref(navigableLinkHref(e.target)) && !armedClickPins(e)) return;
|
|
3772
4217
|
// Click what the hover box shows: the currently hovered element is the
|
|
3773
4218
|
// target the user aimed at. Fall back to a fresh hit-test at the click
|
|
3774
4219
|
// point (e.g. a click with no preceding mousemove).
|
|
@@ -3796,12 +4241,36 @@
|
|
|
3796
4241
|
window.getSelection().removeAllRanges();
|
|
3797
4242
|
renderAnnotationOverlay();
|
|
3798
4243
|
}
|
|
4244
|
+
// An Escape in Interact that the page did not use (no handler cancelled it
|
|
4245
|
+
// or stopped it on its way up, focus not in one of its text fields) is
|
|
4246
|
+
// offered to the host as page-escape, after every handler had its turn: the
|
|
4247
|
+
// Inbox leaves full screen with it. The page still got the key first.
|
|
4248
|
+
var escapeReachedWindow = null;
|
|
4249
|
+
window.addEventListener('keydown', function(e) {
|
|
4250
|
+
if (e.key === 'Escape') escapeReachedWindow = e;
|
|
4251
|
+
});
|
|
4252
|
+
function offerUnusedEscape(e) {
|
|
4253
|
+
if (e.repeat || e.isComposing || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
|
|
4254
|
+
var t = e.target;
|
|
4255
|
+
if (t && t.nodeType === 1) {
|
|
4256
|
+
var tag = t.tagName;
|
|
4257
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t.isContentEditable) return;
|
|
4258
|
+
if (t.closest && t.closest('[contenteditable]:not([contenteditable="false"])')) return;
|
|
4259
|
+
}
|
|
4260
|
+
// The page's own dialog closes on this Escape (the browser's default, not a handler).
|
|
4261
|
+
if (document.querySelector('dialog[open]')) return;
|
|
4262
|
+
setTimeout(function() {
|
|
4263
|
+
if (escapeReachedWindow !== e || e.defaultPrevented) return;
|
|
4264
|
+
postToParent({ type: PREFIX + 'page-escape' });
|
|
4265
|
+
}, 0);
|
|
4266
|
+
}
|
|
3799
4267
|
document.addEventListener('keydown', function(e) {
|
|
3800
4268
|
if (e.key !== 'Escape' || vimEnabled) return;
|
|
3801
4269
|
if (!annotateModeActive) {
|
|
3802
4270
|
// Interact mode: Esc belongs to the page — except an open drag-comment
|
|
3803
4271
|
// draft (drag-selection stays live in Interact) still closes first.
|
|
3804
4272
|
if (pendingSelection) closePendingDraft();
|
|
4273
|
+
else offerUnusedEscape(e);
|
|
3805
4274
|
return;
|
|
3806
4275
|
}
|
|
3807
4276
|
if (pendingSelection) {
|
|
@@ -4715,6 +5184,29 @@
|
|
|
4715
5184
|
if (document.body) document.body.removeAttribute('data-plannotator-vim-focus-owner');
|
|
4716
5185
|
}
|
|
4717
5186
|
|
|
5187
|
+
// Selected markup for the parent's rich-text Copy button, built only when
|
|
5188
|
+
// the button is pressed (rich-copy-request). Keeps the enclosing formatting
|
|
5189
|
+
// that cloneContents omits for text endpoints (the same semantic ancestors
|
|
5190
|
+
// selectionRangeToHtml keeps) and absolutizes links.
|
|
5191
|
+
function htmlForRange(range) {
|
|
5192
|
+
var holder = document.createElement('div');
|
|
5193
|
+
var fragment = range.cloneContents();
|
|
5194
|
+
var ancestor = range.commonAncestorContainer.nodeType === 1
|
|
5195
|
+
? range.commonAncestorContainer : range.commonAncestorContainer.parentElement;
|
|
5196
|
+
while (ancestor && ancestor !== document.body) {
|
|
5197
|
+
if (ancestor.matches('a,b,blockquote,code,del,em,h1,h2,h3,h4,h5,h6,i,li,ol,p,pre,s,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,u,ul')) {
|
|
5198
|
+
var wrapper = ancestor.cloneNode(false);
|
|
5199
|
+
while (fragment.firstChild) wrapper.appendChild(fragment.firstChild);
|
|
5200
|
+
fragment.appendChild(wrapper);
|
|
5201
|
+
}
|
|
5202
|
+
ancestor = ancestor.parentElement;
|
|
5203
|
+
}
|
|
5204
|
+
holder.appendChild(fragment);
|
|
5205
|
+
var links = holder.querySelectorAll('a[href]');
|
|
5206
|
+
for (var i = 0; i < links.length; i++) links[i].setAttribute('href', links[i].href);
|
|
5207
|
+
return holder.innerHTML;
|
|
5208
|
+
}
|
|
5209
|
+
|
|
4718
5210
|
function copyVimText(text) {
|
|
4719
5211
|
if (!text) return;
|
|
4720
5212
|
postToParent({
|