Skip to content

Commit 6357faa

Browse files
authored
Merge pull request #428 from embedpdf/fix/add-props-to-modal-ui
Pass modal props & fix link modal context
2 parents 60c982a + cac0937 commit 6357faa

File tree

21 files changed

+156
-36
lines changed

21 files changed

+156
-36
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@embedpdf/plugin-ui': minor
3+
---
4+
5+
Added modal props feature to pass context when opening modals:
6+
7+
- Extended `openModal(modalId, props?)` to accept optional props parameter
8+
- Added `props` field to `ModalSlotState` type
9+
- Added `modalProps` to `ModalRendererProps` for all frameworks (Preact, React, Svelte, Vue)
10+
- Updated schema renderers to pass `modalProps` through to modal components
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@embedpdf/snippet': minor
3+
---
4+
5+
Fixed link modal context handling:
6+
7+
- Added `source` prop to LinkModal to distinguish between annotation and text selection context
8+
- Updated `annotation:add-link` command to pass `{ source: 'selection' }` when opening modal
9+
- Updated `annotation:toggle-link` command to pass `{ source: 'annotation' }` when opening modal
10+
- Prevents incorrect behavior where annotation selection would override text selection when creating links

examples/svelte-tailwind/src/lib/components/LinkModal.svelte

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@
1717
import { Dialog, DialogContent, DialogFooter, Button } from './ui';
1818
1919
type LinkTab = 'url' | 'page';
20+
type LinkSource = 'annotation' | 'selection';
2021
2122
interface Props {
2223
documentId: string;
2324
isOpen?: boolean;
2425
onClose?: () => void;
2526
onExited?: () => void;
27+
/** Source context that triggered the modal */
28+
source?: LinkSource;
2629
}
2730
28-
let { documentId, isOpen = false, onClose, onExited }: Props = $props();
31+
let { documentId, isOpen = false, onClose, onExited, source }: Props = $props();
2932
3033
const scrollCapability = useScrollCapability();
3134
const annotationCapability = useAnnotationCapability();
@@ -85,9 +88,10 @@
8588
};
8689
}
8790
88-
// Create links based on context
89-
if (selectedAnnotation) {
90-
// IRT-linked links from selected annotation
91+
// Helper to create link on annotation
92+
const createLinkOnAnnotation = () => {
93+
if (!selectedAnnotation) return false;
94+
9195
const rects =
9296
'segmentRects' in selectedAnnotation.object
9397
? selectedAnnotation.object.segmentRects
@@ -107,7 +111,13 @@
107111
strokeWidth: 2,
108112
});
109113
}
110-
} else if (textSelection.length > 0) {
114+
return true;
115+
};
116+
117+
// Helper to create link from text selection
118+
const createLinkFromSelection = () => {
119+
if (textSelection.length === 0) return false;
120+
111121
const selectionText = selectionScope?.getSelectedText();
112122
113123
// Create transparent highlight parent with IRT-linked links for each selection
@@ -153,6 +163,20 @@
153163
}, ignore);
154164
}
155165
selectionScope?.clear();
166+
return true;
167+
};
168+
169+
// Create links based on the source context passed when opening the modal
170+
// This ensures the correct context is used even when both annotation and text are selected
171+
if (source === 'annotation') {
172+
createLinkOnAnnotation();
173+
} else if (source === 'selection') {
174+
createLinkFromSelection();
175+
} else {
176+
// Fallback for backwards compatibility: annotation first, then selection
177+
if (!createLinkOnAnnotation()) {
178+
createLinkFromSelection();
179+
}
156180
}
157181
158182
onClose?.();

examples/svelte-tailwind/src/lib/config/commands.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,8 +1343,8 @@ export const commands: Record<string, Command<State>> = {
13431343
const ui = registry.getPlugin<UIPlugin>('ui')?.provides();
13441344
if (!ui) return;
13451345

1346-
// Open the link modal - it will detect text selection context
1347-
ui.forDocument(documentId).openModal('link-modal');
1346+
// Open the link modal with selection context
1347+
ui.forDocument(documentId).openModal('link-modal', { source: 'selection' });
13481348
},
13491349
disabled: ({ state, documentId }) => {
13501350
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
@@ -1388,7 +1388,7 @@ export const commands: Record<string, Command<State>> = {
13881388
if (scope.hasAttachedLinks(selected.object.id)) {
13891389
scope.deleteAttachedLinks(selected.object.id);
13901390
} else {
1391-
ui.forDocument(documentId).openModal('link-modal');
1391+
ui.forDocument(documentId).openModal('link-modal', { source: 'annotation' });
13921392
}
13931393
},
13941394
visible: ({ registry, documentId }) => {

examples/svelte-tailwind/src/lib/ui/SchemaModal.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
interface Props extends ModalRendererProps {}
1313
14-
let { schema, documentId, isOpen, onClose, onExited }: Props = $props();
14+
let { schema, documentId, isOpen, onClose, onExited, modalProps }: Props = $props();
1515
1616
const { getCustomComponent } = useItemRenderer();
1717
@@ -28,5 +28,5 @@
2828
</script>
2929

3030
{#if ModalComponent}
31-
<ModalComponent {documentId} {isOpen} {onClose} {onExited} />
31+
<ModalComponent {documentId} {isOpen} {onClose} {onExited} {...modalProps} />
3232
{/if}

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,15 @@ import {
104104
import { Dialog, DialogContent, DialogFooter, Button } from './ui';
105105
106106
type LinkTab = 'url' | 'page';
107+
type LinkSource = 'annotation' | 'selection';
107108
108109
interface Props {
109110
documentId: string;
110111
isOpen?: boolean;
111112
onClose?: () => void;
112113
onExited?: () => void;
114+
/** Source context that triggered the modal */
115+
source?: LinkSource;
113116
}
114117
115118
const props = defineProps<Props>();
@@ -186,9 +189,10 @@ const handleSubmit = () => {
186189
};
187190
}
188191
189-
// Create links based on context
190-
if (selectedAnnotation.value) {
191-
// IRT-linked links from selected annotation
192+
// Helper to create link on annotation
193+
const createLinkOnAnnotation = () => {
194+
if (!selectedAnnotation.value) return false;
195+
192196
const anno = selectedAnnotation.value;
193197
const rects = 'segmentRects' in anno.object ? anno.object.segmentRects : [anno.object.rect];
194198
@@ -206,7 +210,13 @@ const handleSubmit = () => {
206210
strokeWidth: 2,
207211
});
208212
}
209-
} else if (textSelection.value.length > 0) {
213+
return true;
214+
};
215+
216+
// Helper to create link from text selection
217+
const createLinkFromSelection = () => {
218+
if (textSelection.value.length === 0) return false;
219+
210220
const selectionText = selectionScope.value?.getSelectedText();
211221
212222
// Create transparent highlight parent with IRT-linked links for each selection
@@ -252,6 +262,20 @@ const handleSubmit = () => {
252262
}, ignore);
253263
}
254264
selectionScope.value?.clear();
265+
return true;
266+
};
267+
268+
// Create links based on the source context passed when opening the modal
269+
// This ensures the correct context is used even when both annotation and text are selected
270+
if (props.source === 'annotation') {
271+
createLinkOnAnnotation();
272+
} else if (props.source === 'selection') {
273+
createLinkFromSelection();
274+
} else {
275+
// Fallback for backwards compatibility: annotation first, then selection
276+
if (!createLinkOnAnnotation()) {
277+
createLinkFromSelection();
278+
}
255279
}
256280
257281
props.onClose?.();

examples/vue-tailwind/src/config/commands.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,8 +1343,8 @@ export const commands: Record<string, Command<State>> = {
13431343
const ui = registry.getPlugin<UIPlugin>('ui')?.provides();
13441344
if (!ui) return;
13451345

1346-
// Open the link modal - it will detect text selection context
1347-
ui.forDocument(documentId).openModal('link-modal');
1346+
// Open the link modal with selection context
1347+
ui.forDocument(documentId).openModal('link-modal', { source: 'selection' });
13481348
},
13491349
disabled: ({ state, documentId }) => {
13501350
return lacksPermission(state, documentId, PdfPermissionFlag.ModifyAnnotations);
@@ -1388,7 +1388,7 @@ export const commands: Record<string, Command<State>> = {
13881388
if (scope.hasAttachedLinks(selected.object.id)) {
13891389
scope.deleteAttachedLinks(selected.object.id);
13901390
} else {
1391-
ui.forDocument(documentId).openModal('link-modal');
1391+
ui.forDocument(documentId).openModal('link-modal', { source: 'annotation' });
13921392
}
13931393
},
13941394
visible: ({ registry, documentId }) => {

examples/vue-tailwind/src/ui/SchemaModal.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Props {
2020
isOpen: boolean;
2121
onClose: () => void;
2222
onExited: () => void;
23+
modalProps?: Record<string, unknown>;
2324
}
2425
2526
const props = defineProps<Props>();
@@ -38,6 +39,7 @@ const modalContent = computed(() => {
3839
isOpen: props.isOpen,
3940
onClose: props.onClose,
4041
onExited: props.onExited,
42+
...props.modalProps,
4143
});
4244
});
4345
</script>

packages/plugin-ui/src/lib/actions.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export interface SetSidebarTabAction extends Action {
7373
// Modal action types (with animation lifecycle)
7474
export interface OpenModalAction extends Action {
7575
type: typeof OPEN_MODAL;
76-
payload: { documentId: string; modalId: string };
76+
payload: { documentId: string; modalId: string; props?: Record<string, unknown> };
7777
}
7878

7979
export interface CloseModalAction extends Action {
@@ -196,9 +196,13 @@ export const setSidebarTab = (
196196
});
197197

198198
// Modal action creators (with animation lifecycle)
199-
export const openModal = (documentId: string, modalId: string): OpenModalAction => ({
199+
export const openModal = (
200+
documentId: string,
201+
modalId: string,
202+
props?: Record<string, unknown>,
203+
): OpenModalAction => ({
200204
type: OPEN_MODAL,
201-
payload: { documentId, modalId },
205+
payload: { documentId, modalId, props },
202206
});
203207

204208
export const closeModal = (documentId: string): CloseModalAction => ({

packages/plugin-ui/src/lib/reducer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export const uiReducer = (state = initialState, action: UIAction): UIState => {
222222
// ─────────────────────────────────────────────────────────
223223

224224
case OPEN_MODAL: {
225-
const { documentId, modalId } = action.payload;
225+
const { documentId, modalId, props } = action.payload;
226226
const docState = state.documents[documentId] || initialDocumentState;
227227

228228
return {
@@ -234,6 +234,7 @@ export const uiReducer = (state = initialState, action: UIAction): UIState => {
234234
activeModal: {
235235
modalId,
236236
isOpen: true,
237+
props,
237238
},
238239
openMenus: {}, // Close all menus when opening modal
239240
},

0 commit comments

Comments
 (0)