@plannotator/ui 0.53.1 → 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 +217 -11
- package/README.md +65 -10
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +142 -24
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- 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 +139 -4
- package/components/CommentPopover.tsx +44 -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/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/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/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 +280 -41
- 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/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 +47 -6
- package/components/html-viewer/bridge-script.asset.js +173 -1
- package/components/html-viewer/bridge-script.ts +173 -1
- 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/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- 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 +4 -3
- 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/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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import type { Annotation, EditorMode, HostDraft, ImageAttachment, InputMethod } from "../../types";
|
|
19
19
|
import { AnnotationType } from "../../types";
|
|
20
20
|
import { copyTextPreservingFocus } from "../../utils/clipboard";
|
|
21
|
+
import { getRichCopyEnabled } from "../../utils/richCopyPolicy";
|
|
21
22
|
import { elementIdentityForAskAI } from "../../utils/parser";
|
|
22
23
|
import { getIdentity } from "../../utils/identity";
|
|
23
24
|
import { THUMBS_UP_LABEL } from "../../utils/quickLabels";
|
|
@@ -139,6 +140,14 @@ function parseVimBridgeHelp(value: unknown): boolean | null {
|
|
|
139
140
|
|
|
140
141
|
const MAX_VIM_COPY_TEXT_LENGTH = 2 * 1024 * 1024;
|
|
141
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The srcdoc frame's sandbox. `allow-modals` lets the page's alert, confirm
|
|
145
|
+
* and prompt open (#2063: people's own pages and local sites). Never
|
|
146
|
+
* `allow-same-origin`: the page stays at an opaque origin, away from the
|
|
147
|
+
* session's API. Live-app mode is unsandboxed and does not use this.
|
|
148
|
+
*/
|
|
149
|
+
export const HTML_VIEWER_SANDBOX = "allow-scripts allow-modals";
|
|
150
|
+
|
|
142
151
|
/** Default wait for the bridge's `ready` on the `bridgeScriptUrl` path. */
|
|
143
152
|
export const DEFAULT_BRIDGE_READY_TIMEOUT_MS = 5000;
|
|
144
153
|
|
|
@@ -901,9 +910,15 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
901
910
|
return true;
|
|
902
911
|
}, [readOnly, vimModeEnabled]);
|
|
903
912
|
|
|
913
|
+
// A composer hidden with its target (a slide the page hid) is not on
|
|
914
|
+
// screen: it neither blocks Vim's document focus nor counts as an open
|
|
915
|
+
// overlay. Escape still closes it, as its own rung (below).
|
|
916
|
+
const composerShown = !!hook.commentPopover && hook.pinVisible;
|
|
917
|
+
const composerHidden = !!hook.commentPopover && !hook.pinVisible;
|
|
918
|
+
|
|
904
919
|
useVimDocumentFocus({
|
|
905
920
|
enabled: !readOnly && vimModeEnabled,
|
|
906
|
-
blocked: !!hook.toolbarState ||
|
|
921
|
+
blocked: !!hook.toolbarState || composerShown || !!hook.quickLabelPicker,
|
|
907
922
|
focusDocument: focusVimDocument,
|
|
908
923
|
});
|
|
909
924
|
|
|
@@ -1005,6 +1020,27 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1005
1020
|
postToBridge({ type: `${PREFIX}set-host-navigates`, active: true });
|
|
1006
1021
|
}, [iframeReadyVersion, hostNavigates]);
|
|
1007
1022
|
|
|
1023
|
+
// The hidden composer's Escape rung: with focus outside the iframe,
|
|
1024
|
+
// Escape closes the hidden draft (its text stays in the draft store) and
|
|
1025
|
+
// is used, so the next press reaches the rest of the ladder. Capture
|
|
1026
|
+
// phase, so it runs before the Vim document-focus rung. Inside the
|
|
1027
|
+
// iframe the bridge closes the draft and the hook closes the composer.
|
|
1028
|
+
const handleCommentCloseRef = useRef(hook.handleCommentClose);
|
|
1029
|
+
handleCommentCloseRef.current = hook.handleCommentClose;
|
|
1030
|
+
useEffect(() => {
|
|
1031
|
+
if (readOnly || !composerHidden) return;
|
|
1032
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
1033
|
+
if (e.key !== 'Escape' || e.defaultPrevented || e.isComposing) return;
|
|
1034
|
+
const target = e.target as HTMLElement | null;
|
|
1035
|
+
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;
|
|
1036
|
+
if (document.querySelector('[data-plannotator-confirm-dialog="true"]')) return;
|
|
1037
|
+
e.preventDefault();
|
|
1038
|
+
handleCommentCloseRef.current();
|
|
1039
|
+
};
|
|
1040
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
1041
|
+
return () => window.removeEventListener('keydown', onKeyDown, true);
|
|
1042
|
+
}, [readOnly, composerHidden]);
|
|
1043
|
+
|
|
1008
1044
|
// Parent-side Esc rung: with focus outside the iframe the bridge never
|
|
1009
1045
|
// sees the keydown. Any open composer/toolbar/picker still closes first —
|
|
1010
1046
|
// their state is read from this render's closure, so an Esc that closed
|
|
@@ -1015,6 +1051,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1015
1051
|
!!hook.toolbarState || !!hook.commentPopover || !!hook.quickLabelPicker || !!globalCommentPopover;
|
|
1016
1052
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
1017
1053
|
if (e.key !== 'Escape' || e.defaultPrevented) return;
|
|
1054
|
+
// A hidden composer is closed by its own rung above.
|
|
1018
1055
|
if (overlayOpen) return;
|
|
1019
1056
|
// A text field or dialog owns its own Escape.
|
|
1020
1057
|
const target = e.target as HTMLElement | null;
|
|
@@ -1057,7 +1094,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1057
1094
|
|
|
1058
1095
|
const vimOverlayWasOpenRef = useRef(false);
|
|
1059
1096
|
useEffect(() => {
|
|
1060
|
-
const overlayOpen = !!hook.toolbarState ||
|
|
1097
|
+
const overlayOpen = !!hook.toolbarState || composerShown || !!hook.quickLabelPicker;
|
|
1061
1098
|
const wasOpen = vimOverlayWasOpenRef.current;
|
|
1062
1099
|
vimOverlayWasOpenRef.current = overlayOpen;
|
|
1063
1100
|
if (
|
|
@@ -1072,7 +1109,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1072
1109
|
{ type: `${PREFIX}focus-vim` },
|
|
1073
1110
|
);
|
|
1074
1111
|
}
|
|
1075
|
-
}, [
|
|
1112
|
+
}, [composerShown, hook.quickLabelPicker, hook.toolbarState, readOnly, vimModeEnabled]);
|
|
1076
1113
|
|
|
1077
1114
|
useEffect(() => {
|
|
1078
1115
|
if (iframeReadyVersion === 0) return;
|
|
@@ -1258,7 +1295,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1258
1295
|
same-origin XHR) and no srcdoc. Srcdoc mode is unchanged. */}
|
|
1259
1296
|
<iframe
|
|
1260
1297
|
ref={iframeRef}
|
|
1261
|
-
{...(src ? { src } : { srcDoc: srcdoc, sandbox:
|
|
1298
|
+
{...(src ? { src } : { srcDoc: srcdoc, sandbox: HTML_VIEWER_SANDBOX + (hostNavigates ? " allow-popups" : "") })}
|
|
1262
1299
|
style={{
|
|
1263
1300
|
width: "100%",
|
|
1264
1301
|
height: fullViewport ? "100%" : `${iframeHeight}px`,
|
|
@@ -1315,6 +1352,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1315
1352
|
positionMode="center-above"
|
|
1316
1353
|
element={hook.toolbarState.element}
|
|
1317
1354
|
copyText={hook.toolbarState.selectionText}
|
|
1355
|
+
getRichCopy={getRichCopyEnabled() ? hook.toolbarState.getRichCopy : undefined}
|
|
1318
1356
|
// HTML/live surfaces are comment-only: no Delete, no label
|
|
1319
1357
|
// picker, no Alt+digit labels (commentOnly). Exactly ONE label
|
|
1320
1358
|
// affordance is restored: the hardcoded 👍 "Looks good". The
|
|
@@ -1333,8 +1371,10 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1333
1371
|
document.body,
|
|
1334
1372
|
)}
|
|
1335
1373
|
|
|
1336
|
-
{/*
|
|
1337
|
-
|
|
1374
|
+
{/* Hidden with its target (a slide the page hid): CommentPopover's draft
|
|
1375
|
+
store keeps the text and images, and shown again it does not take
|
|
1376
|
+
focus from where the person is. */}
|
|
1377
|
+
{!readOnly && hook.pinVisible && hook.commentPopover &&
|
|
1338
1378
|
createPortal(
|
|
1339
1379
|
<CommentPopover
|
|
1340
1380
|
anchorEl={hook.commentPopover.anchorEl}
|
|
@@ -1362,6 +1402,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1362
1402
|
onRemoveTargetChip={targetChips ? hook.removeDraftTarget : undefined}
|
|
1363
1403
|
onHoverTargetChip={targetChips ? hook.flashDraftTarget : undefined}
|
|
1364
1404
|
refocusToken={targetChips ? hook.composerFocusToken : undefined}
|
|
1405
|
+
autoFocus={hook.composerAutoFocus}
|
|
1365
1406
|
captureStrayKeys={multiSelectActive}
|
|
1366
1407
|
yieldState={multiSelectActive ? composerYield : undefined}
|
|
1367
1408
|
/>,
|
|
@@ -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';
|
|
@@ -1238,6 +1295,8 @@
|
|
|
1238
1295
|
if (annotateModeActive && currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
|
|
1239
1296
|
setHoverHighlight(committedHighlightIdAtCached(lastPointer.x, lastPointer.y));
|
|
1240
1297
|
}
|
|
1298
|
+
// Before the boxes: it may re-find a replaced target they then follow.
|
|
1299
|
+
reconcilePendingPinVisibility();
|
|
1241
1300
|
positionMultiTargetBoxes();
|
|
1242
1301
|
if (pendingPinEl && pendingPinEl.isConnected) {
|
|
1243
1302
|
positionPinpointBox(pendingPinEl);
|
|
@@ -3230,7 +3289,91 @@
|
|
|
3230
3289
|
}
|
|
3231
3290
|
}
|
|
3232
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
|
+
|
|
3233
3374
|
function clearPendingPin() {
|
|
3375
|
+
pendingPinVisible = true;
|
|
3376
|
+
pendingPinSearchGen = 0;
|
|
3234
3377
|
pendingPinEl = null;
|
|
3235
3378
|
pendingPinContext = null;
|
|
3236
3379
|
pendingPinAnchor = null;
|
|
@@ -3286,6 +3429,7 @@
|
|
|
3286
3429
|
}
|
|
3287
3430
|
|
|
3288
3431
|
function positionBoxToEl(box, el) {
|
|
3432
|
+
if (pendingPinHidden(el)) { box.style.display = 'none'; return; }
|
|
3289
3433
|
var r = el.getBoundingClientRect();
|
|
3290
3434
|
box.style.display = 'block';
|
|
3291
3435
|
box.style.left = r.left + 'px';
|
|
@@ -3417,6 +3561,7 @@
|
|
|
3417
3561
|
*/
|
|
3418
3562
|
function removeMultiTargetByKey(key, echo) {
|
|
3419
3563
|
if (!key) return;
|
|
3564
|
+
schedulePinpointReconcile();
|
|
3420
3565
|
if (key === pendingPinKey) {
|
|
3421
3566
|
if (!pendingMultiTargets.length) {
|
|
3422
3567
|
// Last target gone — the draft is cancelled.
|
|
@@ -3433,6 +3578,7 @@
|
|
|
3433
3578
|
var next = pendingMultiTargets.shift();
|
|
3434
3579
|
destroyMultiTargetBox(next.box);
|
|
3435
3580
|
pendingPinEl = next.el;
|
|
3581
|
+
pendingPinSearchGen = 0;
|
|
3436
3582
|
pendingPinAnchor = next.anchor;
|
|
3437
3583
|
pendingPinKey = next.key;
|
|
3438
3584
|
pendingPinLabel = next.label;
|
|
@@ -3500,6 +3646,7 @@
|
|
|
3500
3646
|
var key = makeTargetKey();
|
|
3501
3647
|
var box = createMultiTargetBox(el);
|
|
3502
3648
|
pendingMultiTargets.push({ key: key, el: el, anchor: anchor, label: label, text: text, point: point, context: context, box: box });
|
|
3649
|
+
schedulePinpointReconcile();
|
|
3503
3650
|
postToParent({
|
|
3504
3651
|
type: PREFIX + 'multi-target-added',
|
|
3505
3652
|
key: key,
|
|
@@ -3634,6 +3781,8 @@
|
|
|
3634
3781
|
multiSelectArmed = false;
|
|
3635
3782
|
multiSelectMax = MAX_MULTI_TARGETS;
|
|
3636
3783
|
pendingPinEl = el;
|
|
3784
|
+
pendingPinVisible = true;
|
|
3785
|
+
pendingPinSearchGen = 0;
|
|
3637
3786
|
pendingPinAnchor = buildElementAnchor(el);
|
|
3638
3787
|
pendingPinKey = makeTargetKey();
|
|
3639
3788
|
pendingPinLabel = pinpointHoverLabel(el);
|
|
@@ -5035,6 +5184,29 @@
|
|
|
5035
5184
|
if (document.body) document.body.removeAttribute('data-plannotator-vim-focus-owner');
|
|
5036
5185
|
}
|
|
5037
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
|
+
|
|
5038
5210
|
function copyVimText(text) {
|
|
5039
5211
|
if (!text) return;
|
|
5040
5212
|
postToParent({
|
|
@@ -460,6 +460,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
460
460
|
// --- Selection ---
|
|
461
461
|
var pendingSelection = null;
|
|
462
462
|
var pendingRange = null; // live range for the pending selection (scroll tracking)
|
|
463
|
+
// The pending text selection's own text, uncapped: what plain-text Copy
|
|
464
|
+
// writes. Range.toString() would lose the rendered line breaks between
|
|
465
|
+
// blocks and keep the source indentation.
|
|
466
|
+
var pendingCopyText = '';
|
|
463
467
|
var pendingPinEl = null; // element pinned by a pinpoint click (outline + scroll tracking)
|
|
464
468
|
var pendingPinAnchor = null; // serialized anchor for the pending pin
|
|
465
469
|
var pendingPinKey = null; // target key for the primary pinpoint target (multi-select)
|
|
@@ -571,7 +575,8 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
571
575
|
}
|
|
572
576
|
skipNextClear = false; // a real text selection happened
|
|
573
577
|
var range = sel.getRangeAt(0);
|
|
574
|
-
var
|
|
578
|
+
var fullText = sel.toString().trim();
|
|
579
|
+
var text = capSelectionText(fullText);
|
|
575
580
|
if (!text) return false;
|
|
576
581
|
|
|
577
582
|
// A draft now owns the surface: kill the click-to-select hover
|
|
@@ -580,6 +585,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
580
585
|
|
|
581
586
|
var rect = range.getBoundingClientRect();
|
|
582
587
|
pendingRange = range;
|
|
588
|
+
pendingCopyText = fullText;
|
|
583
589
|
pendingSelection = {
|
|
584
590
|
text: text,
|
|
585
591
|
startContainerPath: getNodePath(range.startContainer),
|
|
@@ -597,6 +603,13 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
597
603
|
targetKey: (extras && extras.targetKey) || undefined,
|
|
598
604
|
targetLabel: (extras && extras.targetLabel) || undefined,
|
|
599
605
|
context: (extras && extras.context) || undefined,
|
|
606
|
+
// This bridge answers rich-copy-request for a text selection (the
|
|
607
|
+
// toolbar's rich-text Copy). Optional and additive: an older bridge
|
|
608
|
+
// omits it and Copy stays plain text, so no protocol version bump.
|
|
609
|
+
richCopy: (extras && extras.pinpoint) ? undefined : true,
|
|
610
|
+
// The quote above was cut to MAX_SELECTION_TEXT: a plain-text copy made
|
|
611
|
+
// from it alone (no answer to rich-copy-request) says so.
|
|
612
|
+
textCut: text.length < fullText.length ? true : undefined,
|
|
600
613
|
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
|
|
601
614
|
});
|
|
602
615
|
renderAnnotationOverlay(); // draft selection highlight (overlay-projected)
|
|
@@ -645,6 +658,46 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
645
658
|
if (!e.data || !e.data.type) return;
|
|
646
659
|
var type = e.data.type;
|
|
647
660
|
|
|
661
|
+
// Copy (rich text) was pressed in the parent: build the pending text
|
|
662
|
+
// range's markup now (never on selection). A throw or an oversized range
|
|
663
|
+
// answers without markup, so the parent copies plain text.
|
|
664
|
+
if (type === PREFIX + 'rich-copy-request') {
|
|
665
|
+
// Text and markup are each capped at 256 KiB: past it the text is cut
|
|
666
|
+
// (never at half a surrogate pair) and marked, and the markup is not
|
|
667
|
+
// built at all (too large for rich text).
|
|
668
|
+
var copyHtml = '';
|
|
669
|
+
var copyText = '';
|
|
670
|
+
var copyTruncated = false;
|
|
671
|
+
var copyTooLarge = false;
|
|
672
|
+
try {
|
|
673
|
+
if (pendingRange && pendingSelection && pendingSelection.startContainerPath) {
|
|
674
|
+
copyText = pendingCopyText || pendingRange.toString();
|
|
675
|
+
if (copyText.length > 262144) {
|
|
676
|
+
var copyCut = 262144;
|
|
677
|
+
var copyLast = copyText.charCodeAt(copyCut - 1);
|
|
678
|
+
if (copyLast >= 0xd800 && copyLast <= 0xdbff) copyCut -= 1;
|
|
679
|
+
copyText = copyText.slice(0, copyCut);
|
|
680
|
+
copyTruncated = true;
|
|
681
|
+
copyTooLarge = true;
|
|
682
|
+
} else {
|
|
683
|
+
copyHtml = htmlForRange(pendingRange);
|
|
684
|
+
if (copyHtml.length > 262144) copyTooLarge = true;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch (err) {
|
|
688
|
+
copyHtml = '';
|
|
689
|
+
}
|
|
690
|
+
postToParent({
|
|
691
|
+
type: PREFIX + 'rich-copy',
|
|
692
|
+
id: typeof e.data.id === 'string' ? e.data.id : '',
|
|
693
|
+
html: copyTooLarge ? '' : copyHtml,
|
|
694
|
+
text: copyText,
|
|
695
|
+
truncated: copyTruncated,
|
|
696
|
+
tooLarge: copyTooLarge
|
|
697
|
+
});
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
648
701
|
if (type === PREFIX + 'create-mark') {
|
|
649
702
|
var id = e.data.id;
|
|
650
703
|
var annType = e.data.annotationType || 'comment';
|
|
@@ -1362,6 +1415,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1362
1415
|
return pinpointBoxEl;
|
|
1363
1416
|
}
|
|
1364
1417
|
function positionPinpointBox(el) {
|
|
1418
|
+
if (pendingPinHidden(el)) {
|
|
1419
|
+
if (pinpointBoxEl) pinpointBoxEl.style.display = 'none';
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1365
1422
|
var r = el.getBoundingClientRect();
|
|
1366
1423
|
var box = getPinpointBoxEl();
|
|
1367
1424
|
box.style.display = 'block';
|
|
@@ -1515,6 +1572,8 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1515
1572
|
if (annotateModeActive && currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
|
|
1516
1573
|
setHoverHighlight(committedHighlightIdAtCached(lastPointer.x, lastPointer.y));
|
|
1517
1574
|
}
|
|
1575
|
+
// Before the boxes: it may re-find a replaced target they then follow.
|
|
1576
|
+
reconcilePendingPinVisibility();
|
|
1518
1577
|
positionMultiTargetBoxes();
|
|
1519
1578
|
if (pendingPinEl && pendingPinEl.isConnected) {
|
|
1520
1579
|
positionPinpointBox(pendingPinEl);
|
|
@@ -3507,7 +3566,91 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3507
3566
|
}
|
|
3508
3567
|
}
|
|
3509
3568
|
|
|
3569
|
+
// A draft's composer hides with its target and keeps the draft: a slide the
|
|
3570
|
+
// page hid (display:none, visibility:hidden, no box) is not a scroll-out.
|
|
3571
|
+
// Offscreen targets stay visible to support roaming multi-select. A target
|
|
3572
|
+
// the page REPLACED with a new element (a framework re-render, a live-app
|
|
3573
|
+
// route change) is found again through its anchor, the way restore re-finds
|
|
3574
|
+
// committed targets; one that cannot be found keeps the composer visible,
|
|
3575
|
+
// so a draft is never left where nobody can reach it.
|
|
3576
|
+
var pendingPinVisible = true;
|
|
3577
|
+
var pendingPinSearchGen = 0;
|
|
3578
|
+
function pendingPinHidden(el) {
|
|
3579
|
+
if (!el || !el.isConnected) return false;
|
|
3580
|
+
if (targetStyleHidden(el)) return true;
|
|
3581
|
+
var rect = el.getBoundingClientRect();
|
|
3582
|
+
return layoutActive() && !rect.width && !rect.height;
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
function refindPendingTarget(anchor, taken) {
|
|
3586
|
+
var found = resolveAnchorElement(anchor);
|
|
3587
|
+
if (!found || isViewerOverlayNode(found)) return null;
|
|
3588
|
+
for (var i = 0; i < taken.length; i++) if (taken[i] === found) return null;
|
|
3589
|
+
return found;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// Re-find detached draft targets, once per DOM generation each. Returns
|
|
3593
|
+
// whether the primary moved to a new element.
|
|
3594
|
+
function refreshPendingTargets() {
|
|
3595
|
+
var taken = [pendingPinEl];
|
|
3596
|
+
for (var t = 0; t < pendingMultiTargets.length; t++) taken.push(pendingMultiTargets[t].el);
|
|
3597
|
+
var moved = false;
|
|
3598
|
+
if (pendingPinEl && !pendingPinEl.isConnected && pendingPinAnchor && pendingPinSearchGen !== domGeneration) {
|
|
3599
|
+
pendingPinSearchGen = domGeneration;
|
|
3600
|
+
var found = refindPendingTarget(pendingPinAnchor, taken);
|
|
3601
|
+
if (found) {
|
|
3602
|
+
pendingPinEl = found;
|
|
3603
|
+
// The text range belonged to the replaced element: commit as an
|
|
3604
|
+
// element pin on the new one (as a promoted primary does).
|
|
3605
|
+
if (pendingRange && !rangeAlive(pendingRange)) pendingRange = null;
|
|
3606
|
+
if (pendingSelection && pendingSelection.startContainerPath) {
|
|
3607
|
+
pendingSelection = { element: true, text: pendingSelection.text };
|
|
3608
|
+
}
|
|
3609
|
+
moved = true;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
for (var i = 0; i < pendingMultiTargets.length; i++) {
|
|
3613
|
+
var entry = pendingMultiTargets[i];
|
|
3614
|
+
if (!entry.el || entry.el.isConnected || !entry.anchor || entry.searchGen === domGeneration) continue;
|
|
3615
|
+
entry.searchGen = domGeneration;
|
|
3616
|
+
var next = refindPendingTarget(entry.anchor, taken);
|
|
3617
|
+
if (next) { entry.el = next; taken.push(next); }
|
|
3618
|
+
}
|
|
3619
|
+
return moved;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// Re-anchor the parent's composer on the pin (it cannot see the page).
|
|
3623
|
+
function postPendingPinRect() {
|
|
3624
|
+
if (!pendingSelection || !pendingPinEl || !pendingPinEl.isConnected) return;
|
|
3625
|
+
var tracked = pendingRange && rangeAlive(pendingRange) ? pendingRange : pendingPinEl;
|
|
3626
|
+
var r = tracked.getBoundingClientRect();
|
|
3627
|
+
postToParent({
|
|
3628
|
+
type: PREFIX + 'selection-rect',
|
|
3629
|
+
rect: { top: r.top, left: r.left, width: r.width, height: r.height }
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
function reconcilePendingPinVisibility() {
|
|
3634
|
+
if (!pendingPinEl) return;
|
|
3635
|
+
var moved = refreshPendingTargets();
|
|
3636
|
+
var visible = !pendingPinHidden(pendingPinEl);
|
|
3637
|
+
for (var i = 0; !visible && i < pendingMultiTargets.length; i++) {
|
|
3638
|
+
visible = !pendingPinHidden(pendingMultiTargets[i].el);
|
|
3639
|
+
}
|
|
3640
|
+
// A detached primary has no geometry: never leave its outline behind.
|
|
3641
|
+
if (!pendingPinEl.isConnected && pinpointBoxEl) pinpointBoxEl.style.display = 'none';
|
|
3642
|
+
var shown = visible && !pendingPinVisible;
|
|
3643
|
+
if (visible !== pendingPinVisible) {
|
|
3644
|
+
pendingPinVisible = visible;
|
|
3645
|
+
postToParent({ type: PREFIX + 'pin-visibility', visible: visible });
|
|
3646
|
+
if (!visible && pinpointBoxEl) pinpointBoxEl.style.display = 'none';
|
|
3647
|
+
}
|
|
3648
|
+
if (visible && (moved || shown)) postPendingPinRect();
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3510
3651
|
function clearPendingPin() {
|
|
3652
|
+
pendingPinVisible = true;
|
|
3653
|
+
pendingPinSearchGen = 0;
|
|
3511
3654
|
pendingPinEl = null;
|
|
3512
3655
|
pendingPinContext = null;
|
|
3513
3656
|
pendingPinAnchor = null;
|
|
@@ -3563,6 +3706,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3563
3706
|
}
|
|
3564
3707
|
|
|
3565
3708
|
function positionBoxToEl(box, el) {
|
|
3709
|
+
if (pendingPinHidden(el)) { box.style.display = 'none'; return; }
|
|
3566
3710
|
var r = el.getBoundingClientRect();
|
|
3567
3711
|
box.style.display = 'block';
|
|
3568
3712
|
box.style.left = r.left + 'px';
|
|
@@ -3694,6 +3838,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3694
3838
|
*/
|
|
3695
3839
|
function removeMultiTargetByKey(key, echo) {
|
|
3696
3840
|
if (!key) return;
|
|
3841
|
+
schedulePinpointReconcile();
|
|
3697
3842
|
if (key === pendingPinKey) {
|
|
3698
3843
|
if (!pendingMultiTargets.length) {
|
|
3699
3844
|
// Last target gone — the draft is cancelled.
|
|
@@ -3710,6 +3855,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3710
3855
|
var next = pendingMultiTargets.shift();
|
|
3711
3856
|
destroyMultiTargetBox(next.box);
|
|
3712
3857
|
pendingPinEl = next.el;
|
|
3858
|
+
pendingPinSearchGen = 0;
|
|
3713
3859
|
pendingPinAnchor = next.anchor;
|
|
3714
3860
|
pendingPinKey = next.key;
|
|
3715
3861
|
pendingPinLabel = next.label;
|
|
@@ -3777,6 +3923,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3777
3923
|
var key = makeTargetKey();
|
|
3778
3924
|
var box = createMultiTargetBox(el);
|
|
3779
3925
|
pendingMultiTargets.push({ key: key, el: el, anchor: anchor, label: label, text: text, point: point, context: context, box: box });
|
|
3926
|
+
schedulePinpointReconcile();
|
|
3780
3927
|
postToParent({
|
|
3781
3928
|
type: PREFIX + 'multi-target-added',
|
|
3782
3929
|
key: key,
|
|
@@ -3911,6 +4058,8 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
3911
4058
|
multiSelectArmed = false;
|
|
3912
4059
|
multiSelectMax = MAX_MULTI_TARGETS;
|
|
3913
4060
|
pendingPinEl = el;
|
|
4061
|
+
pendingPinVisible = true;
|
|
4062
|
+
pendingPinSearchGen = 0;
|
|
3914
4063
|
pendingPinAnchor = buildElementAnchor(el);
|
|
3915
4064
|
pendingPinKey = makeTargetKey();
|
|
3916
4065
|
pendingPinLabel = pinpointHoverLabel(el);
|
|
@@ -5312,6 +5461,29 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
5312
5461
|
if (document.body) document.body.removeAttribute('data-plannotator-vim-focus-owner');
|
|
5313
5462
|
}
|
|
5314
5463
|
|
|
5464
|
+
// Selected markup for the parent's rich-text Copy button, built only when
|
|
5465
|
+
// the button is pressed (rich-copy-request). Keeps the enclosing formatting
|
|
5466
|
+
// that cloneContents omits for text endpoints (the same semantic ancestors
|
|
5467
|
+
// selectionRangeToHtml keeps) and absolutizes links.
|
|
5468
|
+
function htmlForRange(range) {
|
|
5469
|
+
var holder = document.createElement('div');
|
|
5470
|
+
var fragment = range.cloneContents();
|
|
5471
|
+
var ancestor = range.commonAncestorContainer.nodeType === 1
|
|
5472
|
+
? range.commonAncestorContainer : range.commonAncestorContainer.parentElement;
|
|
5473
|
+
while (ancestor && ancestor !== document.body) {
|
|
5474
|
+
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')) {
|
|
5475
|
+
var wrapper = ancestor.cloneNode(false);
|
|
5476
|
+
while (fragment.firstChild) wrapper.appendChild(fragment.firstChild);
|
|
5477
|
+
fragment.appendChild(wrapper);
|
|
5478
|
+
}
|
|
5479
|
+
ancestor = ancestor.parentElement;
|
|
5480
|
+
}
|
|
5481
|
+
holder.appendChild(fragment);
|
|
5482
|
+
var links = holder.querySelectorAll('a[href]');
|
|
5483
|
+
for (var i = 0; i < links.length; i++) links[i].setAttribute('href', links[i].href);
|
|
5484
|
+
return holder.innerHTML;
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5315
5487
|
function copyVimText(text) {
|
|
5316
5488
|
if (!text) return;
|
|
5317
5489
|
postToParent({
|