Skip to content

Commit d95a02b

Browse files
committed
Unify text & marquee selection
Combine text selection and marquee selection under a single enableForMode API and refactor SelectionLayer to orchestrate both. Add new EnableForModeOptions (enableSelection, showSelectionRects, enableMarquee, showMarqueeRects) and deprecate showRects, setMarqueeEnabled, and isMarqueeEnabled. Events now include modeId for selection and marquee lifecycles. SelectionLayer now composes a new TextSelection component alongside MarqueeSelection so consumers no longer need to render MarqueeSelection separately; a standalone TextSelection export was also added for advanced use. Expose textStyle and marqueeStyle props (background, borderColor, borderStyle) and introduce TextSelectionStyle/MarqueeSelectionStyle types; MarqueeSelection props updated to CSS-standard names (old stroke/fill deprecated). Update Svelte/Vue example usage to remove standalone MarqueeSelection. Also include autogenerated/formatting changes to pdfium vendor function typings.
1 parent bd7576d commit d95a02b

17 files changed

Lines changed: 744 additions & 477 deletions

File tree

.changeset/selection-mode-marquee-unification.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
---
44

55
Unified text selection and marquee selection under the `enableForMode` API. Extended `EnableForModeOptions` with `enableSelection`, `showSelectionRects`, `enableMarquee`, and `showMarqueeRects` options. Deprecated `showRects` (use `showSelectionRects`), `setMarqueeEnabled`, and `isMarqueeEnabled` (use `enableForMode` with `enableMarquee`). Added `modeId` to `SelectionChangeEvent`, `BeginSelectionEvent`, `EndSelectionEvent`, `MarqueeChangeEvent`, `MarqueeEndEvent`, and their scoped counterparts. Marquee handler now uses `registerAlways` so any plugin can enable marquee for their mode. Removed `stopImmediatePropagation` from text selection handler in favor of `isTextSelecting` coordination.
6+
7+
Refactored `SelectionLayer` into a thin orchestrator that composes the new `TextSelection` component and existing `MarqueeSelection` component. Consumers no longer need to render `MarqueeSelection` separately -- `SelectionLayer` now includes both text and marquee selection. Added new `TextSelection` export for advanced standalone usage. Added `textStyle` and `marqueeStyle` props to `SelectionLayer` for consistent CSS-standard styling (`background`, `borderColor`, `borderStyle`). `MarqueeSelection` updated with CSS-standard props (`background`, `borderColor`, `borderStyle`); old `stroke` and `fill` props deprecated. New `TextSelectionStyle` and `MarqueeSelectionStyle` type exports.

examples/svelte-tailwind/src/routes/viewer-schema/+page.svelte

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@
2828
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/svelte';
2929
import { ExportPluginPackage } from '@embedpdf/plugin-export/svelte';
3030
import { PrintPluginPackage } from '@embedpdf/plugin-print/svelte';
31-
import {
32-
SelectionLayer,
33-
SelectionPluginPackage,
34-
MarqueeSelection,
35-
} from '@embedpdf/plugin-selection/svelte';
31+
import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/svelte';
3632
import { SearchLayer, SearchPluginPackage } from '@embedpdf/plugin-search/svelte';
3733
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/svelte';
3834
import { MarqueeCapture, CapturePluginPackage } from '@embedpdf/plugin-capture/svelte';
@@ -324,7 +320,6 @@
324320
selectionMenu={annotationMenu.renderFn}
325321
groupSelectionMenu={groupAnnotationMenu.renderFn}
326322
/>
327-
<MarqueeSelection {documentId} pageIndex={page.pageIndex} />
328323
</PagePointerProvider>
329324
</Rotate>
330325
{/snippet}

examples/vue-tailwind/src/components/ViewerSchemaLayout.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
:selectionMenu="annotationMenu"
6060
:groupSelectionMenu="groupAnnotationMenu"
6161
/>
62-
<MarqueeSelection :documentId="documentId" :pageIndex="page.pageIndex" />
6362
</PagePointerProvider>
6463
</Rotate>
6564
</Scroller>
@@ -96,7 +95,7 @@ import { TilingLayer } from '@embedpdf/plugin-tiling/vue';
9695
import { SearchLayer } from '@embedpdf/plugin-search/vue';
9796
import { MarqueeZoom, ZoomGestureWrapper } from '@embedpdf/plugin-zoom/vue';
9897
import { MarqueeCapture } from '@embedpdf/plugin-capture/vue';
99-
import { SelectionLayer, MarqueeSelection } from '@embedpdf/plugin-selection/vue';
98+
import { SelectionLayer } from '@embedpdf/plugin-selection/vue';
10099
import { RedactionLayer } from '@embedpdf/plugin-redaction/vue';
101100
import { AnnotationLayer } from '@embedpdf/plugin-annotation/vue';
102101
import LoadingSpinner from './LoadingSpinner.vue';

packages/plugin-selection/src/lib/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,24 @@ export interface RegisterMarqueeOnPageOptions {
8282
onRectChange: (rect: Rect | null) => void;
8383
}
8484

85+
// ─────────────────────────────────────────────────────────
86+
// Component Style Types
87+
// ─────────────────────────────────────────────────────────
88+
89+
export interface TextSelectionStyle {
90+
/** Background color for text selection highlights. Default: 'rgba(33,150,243)' */
91+
background?: string;
92+
}
93+
94+
export interface MarqueeSelectionStyle {
95+
/** Fill/background color inside the marquee rectangle. Default: 'rgba(0,122,204,0.15)' */
96+
background?: string;
97+
/** Border color of the marquee rectangle. Default: 'rgba(0,122,204,0.8)' */
98+
borderColor?: string;
99+
/** Border style. Default: 'dashed' */
100+
borderStyle?: 'solid' | 'dashed' | 'dotted';
101+
}
102+
85103
// ─────────────────────────────────────────────────────────
86104
// Events
87105
// ─────────────────────────────────────────────────────────
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './selection-layer';
2+
export * from './text-selection';
23
export * from './copy-to-clipboard';
34
export * from './marquee-selection';

packages/plugin-selection/src/shared/components/marquee-selection.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,51 @@ interface MarqueeSelectionProps {
1313
scale?: number;
1414
/** Optional CSS class applied to the marquee rectangle */
1515
className?: string;
16-
/** Stroke colour (default: 'rgba(0,122,204,0.8)') */
16+
/** Fill/background color inside the marquee rectangle. Default: 'rgba(0,122,204,0.15)' */
17+
background?: string;
18+
/** Border color of the marquee rectangle. Default: 'rgba(0,122,204,0.8)' */
19+
borderColor?: string;
20+
/** Border style. Default: 'dashed' */
21+
borderStyle?: 'solid' | 'dashed' | 'dotted';
22+
/**
23+
* @deprecated Use `borderColor` instead.
24+
*/
1725
stroke?: string;
18-
/** Fill colour (default: 'rgba(0,122,204,0.15)') */
26+
/**
27+
* @deprecated Use `background` instead.
28+
*/
1929
fill?: string;
2030
}
2131

2232
/**
2333
* MarqueeSelection renders a selection rectangle when the user drags to select items.
24-
* Place this component on each page where you want marquee selection to work.
34+
* It registers the marquee handler on the page.
2535
*
26-
* Other plugins (e.g., annotation, form) can subscribe to `onMarqueeEnd` to
36+
* Other plugins (e.g., annotation, form, redaction) can subscribe to `onMarqueeEnd` to
2737
* determine which objects intersect with the marquee rect.
38+
*
39+
* Use this component directly for advanced cases, or use `SelectionLayer`
40+
* which composes both `TextSelection` and `MarqueeSelection`.
2841
*/
2942
export const MarqueeSelection = ({
3043
documentId,
3144
pageIndex,
3245
scale,
3346
className,
34-
stroke = 'rgba(0,122,204,0.8)',
35-
fill = 'rgba(0,122,204,0.15)',
47+
background,
48+
borderColor,
49+
borderStyle = 'dashed',
50+
stroke,
51+
fill,
3652
}: MarqueeSelectionProps) => {
3753
const { plugin: selPlugin } = useSelectionPlugin();
3854
const documentState = useDocumentState(documentId);
3955
const [rect, setRect] = useState<Rect | null>(null);
4056

57+
// Resolve deprecated props: new CSS-standard props take precedence
58+
const resolvedBorderColor = borderColor ?? stroke ?? 'rgba(0,122,204,0.8)';
59+
const resolvedBackground = background ?? fill ?? 'rgba(0,122,204,0.15)';
60+
4161
const actualScale = useMemo(() => {
4262
if (scale !== undefined) return scale;
4363
return documentState?.scale ?? 1;
@@ -65,8 +85,8 @@ export const MarqueeSelection = ({
6585
top: rect.origin.y * actualScale,
6686
width: rect.size.width * actualScale,
6787
height: rect.size.height * actualScale,
68-
border: `1px dashed ${stroke}`,
69-
background: fill,
88+
border: `1px ${borderStyle} ${resolvedBorderColor}`,
89+
background: resolvedBackground,
7090
boxSizing: 'border-box',
7191
zIndex: 1000,
7292
}}
Lines changed: 46 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,65 @@
1-
import { useEffect, useMemo, useState } from '@framework';
2-
import { Rect, Rotation } from '@embedpdf/models';
3-
import { useSelectionPlugin } from '../hooks';
4-
import { useDocumentState } from '@embedpdf/core/@framework';
5-
import { SelectionMenuPlacement } from '@embedpdf/plugin-selection';
1+
import { Fragment } from '@framework';
2+
import { Rotation } from '@embedpdf/models';
3+
import { TextSelectionStyle, MarqueeSelectionStyle } from '@embedpdf/plugin-selection';
64
import { SelectionSelectionMenuRenderFn } from '../types';
7-
import { CounterRotate } from '@embedpdf/utils/@framework';
5+
import { TextSelection } from './text-selection';
6+
import { MarqueeSelection } from './marquee-selection';
87

98
type Props = {
109
documentId: string;
1110
pageIndex: number;
1211
scale?: number;
1312
rotation?: Rotation;
13+
/**
14+
* @deprecated Use `textStyle.background` instead.
15+
*/
1416
background?: string;
17+
/** Styling options for text selection highlights */
18+
textStyle?: TextSelectionStyle;
19+
/** Styling options for the marquee selection rectangle */
20+
marqueeStyle?: MarqueeSelectionStyle;
21+
/** Optional CSS class applied to the marquee rectangle */
22+
marqueeClassName?: string;
1523
selectionMenu?: SelectionSelectionMenuRenderFn;
1624
};
1725

26+
/**
27+
* SelectionLayer is a convenience component that composes both text selection
28+
* and marquee selection on a single page.
29+
*
30+
* For advanced use cases, you can use `TextSelection` and `MarqueeSelection`
31+
* individually.
32+
*/
1833
export function SelectionLayer({
1934
documentId,
2035
pageIndex,
21-
scale: scaleOverride,
22-
rotation: rotationOverride,
23-
background = 'rgba(33,150,243)',
36+
scale,
37+
rotation,
38+
background,
39+
textStyle,
40+
marqueeStyle,
41+
marqueeClassName,
2442
selectionMenu,
2543
}: Props) {
26-
const { plugin: selPlugin } = useSelectionPlugin();
27-
const documentState = useDocumentState(documentId);
28-
const page = documentState?.document?.pages?.[pageIndex];
29-
const [rects, setRects] = useState<Rect[]>([]);
30-
const [boundingRect, setBoundingRect] = useState<Rect | null>(null);
31-
32-
// Store the placement object from the plugin
33-
const [placement, setPlacement] = useState<SelectionMenuPlacement | null>(null);
34-
35-
useEffect(() => {
36-
if (!selPlugin || !documentId) return;
37-
38-
return selPlugin.registerSelectionOnPage({
39-
documentId,
40-
pageIndex,
41-
onRectsChange: ({ rects, boundingRect }) => {
42-
setRects(rects);
43-
setBoundingRect(boundingRect);
44-
},
45-
});
46-
}, [selPlugin, documentId, pageIndex]);
47-
48-
useEffect(() => {
49-
if (!selPlugin || !documentId) return;
50-
51-
// Subscribe to menu placement changes for this specific document
52-
return selPlugin.onMenuPlacement(documentId, (newPlacement) => {
53-
// Optimization: We could filter here, but React state updates are cheap enough usually.
54-
// Ideally, check: if (newPlacement?.pageIndex === pageIndex)
55-
setPlacement(newPlacement);
56-
});
57-
}, [selPlugin, documentId]);
58-
59-
const actualScale = useMemo(() => {
60-
if (scaleOverride !== undefined) return scaleOverride;
61-
return documentState?.scale ?? 1;
62-
}, [scaleOverride, documentState?.scale]);
63-
64-
const actualRotation = useMemo(() => {
65-
if (rotationOverride !== undefined) return rotationOverride;
66-
// Combine page intrinsic rotation with document rotation
67-
const pageRotation = page?.rotation ?? 0;
68-
const docRotation = documentState?.rotation ?? 0;
69-
return ((pageRotation + docRotation) % 4) as Rotation;
70-
}, [rotationOverride, page?.rotation, documentState?.rotation]);
71-
72-
const shouldRenderMenu =
73-
selectionMenu && placement && placement.pageIndex === pageIndex && placement.isVisible;
74-
75-
if (!boundingRect) return null;
76-
7744
return (
78-
<>
79-
<div
80-
style={{
81-
position: 'absolute',
82-
left: boundingRect.origin.x * actualScale,
83-
top: boundingRect.origin.y * actualScale,
84-
width: boundingRect.size.width * actualScale,
85-
height: boundingRect.size.height * actualScale,
86-
mixBlendMode: 'multiply',
87-
isolation: 'isolate',
88-
pointerEvents: 'none',
89-
}}
90-
>
91-
{rects.map((b, i) => (
92-
<div
93-
key={i}
94-
style={{
95-
position: 'absolute',
96-
left: (b.origin.x - boundingRect.origin.x) * actualScale,
97-
top: (b.origin.y - boundingRect.origin.y) * actualScale,
98-
width: b.size.width * actualScale,
99-
height: b.size.height * actualScale,
100-
background,
101-
}}
102-
/>
103-
))}
104-
</div>
105-
{shouldRenderMenu && (
106-
<CounterRotate
107-
rect={{
108-
origin: {
109-
x: placement.rect.origin.x * actualScale,
110-
y: placement.rect.origin.y * actualScale,
111-
},
112-
size: {
113-
width: placement.rect.size.width * actualScale,
114-
height: placement.rect.size.height * actualScale,
115-
},
116-
}}
117-
rotation={actualRotation}
118-
>
119-
{(props) =>
120-
selectionMenu({
121-
...props,
122-
context: {
123-
type: 'selection',
124-
pageIndex,
125-
},
126-
selected: true,
127-
placement,
128-
})
129-
}
130-
</CounterRotate>
131-
)}
132-
</>
45+
<Fragment>
46+
<TextSelection
47+
documentId={documentId}
48+
pageIndex={pageIndex}
49+
scale={scale}
50+
rotation={rotation}
51+
background={textStyle?.background ?? background}
52+
selectionMenu={selectionMenu}
53+
/>
54+
<MarqueeSelection
55+
documentId={documentId}
56+
pageIndex={pageIndex}
57+
scale={scale}
58+
background={marqueeStyle?.background}
59+
borderColor={marqueeStyle?.borderColor}
60+
borderStyle={marqueeStyle?.borderStyle}
61+
className={marqueeClassName}
62+
/>
63+
</Fragment>
13364
);
13465
}

0 commit comments

Comments
 (0)