@plannotator/ui 0.53.1 → 0.55.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 +274 -14
- package/README.md +114 -11
- 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 +22 -5
- package/components/CodeFilePopout.tsx +139 -4
- package/components/CodeSnippetPreview.tsx +174 -0
- package/components/CommentPopover.tsx +44 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DeferredMarkdownContentsRail.tsx +38 -0
- package/components/DiagramBlock.tsx +17 -12
- package/components/DiffFileTree.tsx +7 -6
- package/components/DocumentContents.tsx +74 -0
- 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 +722 -126
- 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 +167 -174
- 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/MarkdownContentsRail.tsx +777 -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/TableOfContents.tsx +20 -6
- 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 +288 -43
- 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 +532 -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 +82 -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 +276 -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/contents-rail.css +337 -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/useHeadingScrollSpy.ts +77 -0
- package/hooks/useHoverPreview.ts +94 -0
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/usePrefersReducedMotion.ts +23 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSwipeStep.ts +597 -0
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/package.json +26 -23
- 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/annotationHelpers.ts +32 -4
- 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/blockParse.ts +45 -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/headingScrollSpy.ts +92 -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,28 @@ 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
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The srcdoc frame's `sandbox` attribute: the `sandbox` prop's value as given,
|
|
153
|
+
* plus `allow-popups` when `hostNavigates` needs it and the value lacks it.
|
|
154
|
+
* Anything that is not a string (`undefined`, and also `null` from a host's
|
|
155
|
+
* unset config, which a destructuring default does not catch) becomes
|
|
156
|
+
* `HTML_VIEWER_SANDBOX`, so the frame always carries a sandbox: React would
|
|
157
|
+
* drop a `null` attribute and run the page unsandboxed.
|
|
158
|
+
*/
|
|
159
|
+
function srcdocSandbox(sandbox: unknown, hostNavigates: boolean): string {
|
|
160
|
+
const value = typeof sandbox === "string" ? sandbox : HTML_VIEWER_SANDBOX;
|
|
161
|
+
if (!hostNavigates || value.split(/\s+/).includes("allow-popups")) return value;
|
|
162
|
+
return value.trim() ? `${value} allow-popups` : "allow-popups";
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
/** Default wait for the bridge's `ready` on the `bridgeScriptUrl` path. */
|
|
143
166
|
export const DEFAULT_BRIDGE_READY_TIMEOUT_MS = 5000;
|
|
144
167
|
|
|
@@ -364,6 +387,26 @@ export interface HtmlViewerProps {
|
|
|
364
387
|
* still pins. Absent → unchanged.
|
|
365
388
|
*/
|
|
366
389
|
hostNavigates?: boolean;
|
|
390
|
+
/**
|
|
391
|
+
* The srcdoc frame's whole `sandbox` attribute value, for a host whose
|
|
392
|
+
* documents must not open dialogs: `"allow-scripts"` keeps a shared page's
|
|
393
|
+
* `alert`, `confirm` and `prompt` from drawing over the host's origin. Used
|
|
394
|
+
* as given, except that `hostNavigates` still adds `allow-popups`, which it
|
|
395
|
+
* needs. Never add `allow-same-origin` beside `allow-scripts`: the page
|
|
396
|
+
* could then remove its own sandbox. Live-app (`src`) mode has no sandbox
|
|
397
|
+
* and ignores this. Absent, or any non-string such as `null` →
|
|
398
|
+
* `HTML_VIEWER_SANDBOX` (`"allow-scripts allow-modals"`), unchanged.
|
|
399
|
+
*
|
|
400
|
+
* `""` (or only spaces) is kept as given: the strictest frame, with no
|
|
401
|
+
* scripts, so the annotation bridge never starts and the page cannot be
|
|
402
|
+
* annotated. It never falls back to the wider default.
|
|
403
|
+
*
|
|
404
|
+
* Read when the frame loads a document: browsers apply a changed `sandbox`
|
|
405
|
+
* only on the frame's next navigation (a new `rawHtml`, for example). To
|
|
406
|
+
* apply a new value to the document already shown, remount the viewer (for
|
|
407
|
+
* example, key it on the value).
|
|
408
|
+
*/
|
|
409
|
+
sandbox?: string;
|
|
367
410
|
}
|
|
368
411
|
|
|
369
412
|
/**
|
|
@@ -420,6 +463,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
420
463
|
bridgeErrorDisplay = "banner",
|
|
421
464
|
onHostDraft,
|
|
422
465
|
hostNavigates = false,
|
|
466
|
+
sandbox = HTML_VIEWER_SANDBOX,
|
|
423
467
|
},
|
|
424
468
|
ref,
|
|
425
469
|
) => {
|
|
@@ -901,9 +945,15 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
901
945
|
return true;
|
|
902
946
|
}, [readOnly, vimModeEnabled]);
|
|
903
947
|
|
|
948
|
+
// A composer hidden with its target (a slide the page hid) is not on
|
|
949
|
+
// screen: it neither blocks Vim's document focus nor counts as an open
|
|
950
|
+
// overlay. Escape still closes it, as its own rung (below).
|
|
951
|
+
const composerShown = !!hook.commentPopover && hook.pinVisible;
|
|
952
|
+
const composerHidden = !!hook.commentPopover && !hook.pinVisible;
|
|
953
|
+
|
|
904
954
|
useVimDocumentFocus({
|
|
905
955
|
enabled: !readOnly && vimModeEnabled,
|
|
906
|
-
blocked: !!hook.toolbarState ||
|
|
956
|
+
blocked: !!hook.toolbarState || composerShown || !!hook.quickLabelPicker,
|
|
907
957
|
focusDocument: focusVimDocument,
|
|
908
958
|
});
|
|
909
959
|
|
|
@@ -1005,6 +1055,27 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1005
1055
|
postToBridge({ type: `${PREFIX}set-host-navigates`, active: true });
|
|
1006
1056
|
}, [iframeReadyVersion, hostNavigates]);
|
|
1007
1057
|
|
|
1058
|
+
// The hidden composer's Escape rung: with focus outside the iframe,
|
|
1059
|
+
// Escape closes the hidden draft (its text stays in the draft store) and
|
|
1060
|
+
// is used, so the next press reaches the rest of the ladder. Capture
|
|
1061
|
+
// phase, so it runs before the Vim document-focus rung. Inside the
|
|
1062
|
+
// iframe the bridge closes the draft and the hook closes the composer.
|
|
1063
|
+
const handleCommentCloseRef = useRef(hook.handleCommentClose);
|
|
1064
|
+
handleCommentCloseRef.current = hook.handleCommentClose;
|
|
1065
|
+
useEffect(() => {
|
|
1066
|
+
if (readOnly || !composerHidden) return;
|
|
1067
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
1068
|
+
if (e.key !== 'Escape' || e.defaultPrevented || e.isComposing) return;
|
|
1069
|
+
const target = e.target as HTMLElement | null;
|
|
1070
|
+
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;
|
|
1071
|
+
if (document.querySelector('[data-plannotator-confirm-dialog="true"]')) return;
|
|
1072
|
+
e.preventDefault();
|
|
1073
|
+
handleCommentCloseRef.current();
|
|
1074
|
+
};
|
|
1075
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
1076
|
+
return () => window.removeEventListener('keydown', onKeyDown, true);
|
|
1077
|
+
}, [readOnly, composerHidden]);
|
|
1078
|
+
|
|
1008
1079
|
// Parent-side Esc rung: with focus outside the iframe the bridge never
|
|
1009
1080
|
// sees the keydown. Any open composer/toolbar/picker still closes first —
|
|
1010
1081
|
// their state is read from this render's closure, so an Esc that closed
|
|
@@ -1015,6 +1086,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1015
1086
|
!!hook.toolbarState || !!hook.commentPopover || !!hook.quickLabelPicker || !!globalCommentPopover;
|
|
1016
1087
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
1017
1088
|
if (e.key !== 'Escape' || e.defaultPrevented) return;
|
|
1089
|
+
// A hidden composer is closed by its own rung above.
|
|
1018
1090
|
if (overlayOpen) return;
|
|
1019
1091
|
// A text field or dialog owns its own Escape.
|
|
1020
1092
|
const target = e.target as HTMLElement | null;
|
|
@@ -1057,7 +1129,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1057
1129
|
|
|
1058
1130
|
const vimOverlayWasOpenRef = useRef(false);
|
|
1059
1131
|
useEffect(() => {
|
|
1060
|
-
const overlayOpen = !!hook.toolbarState ||
|
|
1132
|
+
const overlayOpen = !!hook.toolbarState || composerShown || !!hook.quickLabelPicker;
|
|
1061
1133
|
const wasOpen = vimOverlayWasOpenRef.current;
|
|
1062
1134
|
vimOverlayWasOpenRef.current = overlayOpen;
|
|
1063
1135
|
if (
|
|
@@ -1072,7 +1144,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1072
1144
|
{ type: `${PREFIX}focus-vim` },
|
|
1073
1145
|
);
|
|
1074
1146
|
}
|
|
1075
|
-
}, [
|
|
1147
|
+
}, [composerShown, hook.quickLabelPicker, hook.toolbarState, readOnly, vimModeEnabled]);
|
|
1076
1148
|
|
|
1077
1149
|
useEffect(() => {
|
|
1078
1150
|
if (iframeReadyVersion === 0) return;
|
|
@@ -1258,7 +1330,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1258
1330
|
same-origin XHR) and no srcdoc. Srcdoc mode is unchanged. */}
|
|
1259
1331
|
<iframe
|
|
1260
1332
|
ref={iframeRef}
|
|
1261
|
-
{...(src ? { src } : { srcDoc: srcdoc, sandbox:
|
|
1333
|
+
{...(src ? { src } : { srcDoc: srcdoc, sandbox: srcdocSandbox(sandbox, hostNavigates) })}
|
|
1262
1334
|
style={{
|
|
1263
1335
|
width: "100%",
|
|
1264
1336
|
height: fullViewport ? "100%" : `${iframeHeight}px`,
|
|
@@ -1315,6 +1387,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1315
1387
|
positionMode="center-above"
|
|
1316
1388
|
element={hook.toolbarState.element}
|
|
1317
1389
|
copyText={hook.toolbarState.selectionText}
|
|
1390
|
+
getRichCopy={getRichCopyEnabled() ? hook.toolbarState.getRichCopy : undefined}
|
|
1318
1391
|
// HTML/live surfaces are comment-only: no Delete, no label
|
|
1319
1392
|
// picker, no Alt+digit labels (commentOnly). Exactly ONE label
|
|
1320
1393
|
// affordance is restored: the hardcoded 👍 "Looks good". The
|
|
@@ -1333,8 +1406,10 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1333
1406
|
document.body,
|
|
1334
1407
|
)}
|
|
1335
1408
|
|
|
1336
|
-
{/*
|
|
1337
|
-
|
|
1409
|
+
{/* Hidden with its target (a slide the page hid): CommentPopover's draft
|
|
1410
|
+
store keeps the text and images, and shown again it does not take
|
|
1411
|
+
focus from where the person is. */}
|
|
1412
|
+
{!readOnly && hook.pinVisible && hook.commentPopover &&
|
|
1338
1413
|
createPortal(
|
|
1339
1414
|
<CommentPopover
|
|
1340
1415
|
anchorEl={hook.commentPopover.anchorEl}
|
|
@@ -1362,6 +1437,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1362
1437
|
onRemoveTargetChip={targetChips ? hook.removeDraftTarget : undefined}
|
|
1363
1438
|
onHoverTargetChip={targetChips ? hook.flashDraftTarget : undefined}
|
|
1364
1439
|
refocusToken={targetChips ? hook.composerFocusToken : undefined}
|
|
1440
|
+
autoFocus={hook.composerAutoFocus}
|
|
1365
1441
|
captureStrayKeys={multiSelectActive}
|
|
1366
1442
|
yieldState={multiSelectActive ? composerYield : undefined}
|
|
1367
1443
|
/>,
|
|
@@ -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({
|