Skip to content

Commit df3c88e

Browse files
committed
Add Vue examples for commands, document manager, i18n, and password
Introduced new Vue example components and content for commands, document manager, i18n, password-protected documents, and scroll initial page. Updated annotation and selection examples to support new features. Added corresponding code examples and documentation pages for Vue plugins in the website. Adjusted scroll example to remove hardcoded initial page.
1 parent 620d277 commit df3c88e

62 files changed

Lines changed: 4080 additions & 612 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/vue-tailwind/src/examples/annotation-example-content.vue

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ const handleDelete = () => {
5656
annotationApi.value?.deleteAnnotation(selection.object.pageIndex, selection.object.id);
5757
}
5858
};
59+
60+
const handleDeleteFromMenu = (pageIndex: number, id: string) => {
61+
annotationApi.value?.deleteAnnotation(pageIndex, id);
62+
};
5963
</script>
6064

6165
<template>
@@ -112,7 +116,37 @@ const handleDelete = () => {
112116
style="pointer-events: none"
113117
/>
114118
<SelectionLayer :document-id="documentId" :page-index="page.pageIndex" />
115-
<AnnotationLayer :document-id="documentId" :page-index="page.pageIndex" />
119+
<AnnotationLayer :document-id="documentId" :page-index="page.pageIndex">
120+
<template #selection-menu="{ selected, context, menuWrapperProps, rect }">
121+
<div v-if="selected" v-bind="menuWrapperProps">
122+
<div
123+
class="rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
124+
:style="{
125+
position: 'absolute',
126+
top: `${rect.size.height + 8}px`,
127+
pointerEvents: 'auto',
128+
cursor: 'default',
129+
}"
130+
>
131+
<div class="flex items-center gap-1 px-2 py-1">
132+
<button
133+
@click="
134+
handleDeleteFromMenu(
135+
context.annotation.object.pageIndex,
136+
context.annotation.object.id,
137+
)
138+
"
139+
class="flex items-center justify-center rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-red-400"
140+
aria-label="Delete annotation"
141+
title="Delete annotation"
142+
>
143+
<Trash2 :size="16" />
144+
</button>
145+
</div>
146+
</div>
147+
</div>
148+
</template>
149+
</AnnotationLayer>
116150
</PagePointerProvider>
117151
</template>
118152
</Scroller>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { Viewport } from '@embedpdf/plugin-viewport/vue';
4+
import { Scroller, useScroll } from '@embedpdf/plugin-scroll/vue';
5+
import { RenderLayer } from '@embedpdf/plugin-render/vue';
6+
import { useCommand } from '@embedpdf/plugin-commands/vue';
7+
import { Keyboard, ChevronLeft, ChevronRight, Info } from 'lucide-vue-next';
8+
9+
const props = defineProps<{
10+
documentId: string;
11+
}>();
12+
13+
const { state: scrollState } = useScroll(() => props.documentId);
14+
15+
// Commands
16+
const prevCommand = useCommand(
17+
() => 'nav.prev',
18+
() => props.documentId,
19+
);
20+
const nextCommand = useCommand(
21+
() => 'nav.next',
22+
() => props.documentId,
23+
);
24+
const alertCommand = useCommand(
25+
() => 'doc.alert',
26+
() => props.documentId,
27+
);
28+
29+
// Format shortcut for display
30+
const formatShortcut = (shortcut: string) => {
31+
return shortcut
32+
.replace('arrowleft', '')
33+
.replace('arrowright', '')
34+
.replace('ctrl+', '')
35+
.replace('meta+', '')
36+
.toUpperCase();
37+
};
38+
</script>
39+
40+
<template>
41+
<!-- Toolbar -->
42+
<div
43+
class="flex items-center gap-3 border-b border-gray-300 bg-gray-100 px-3 py-2 dark:border-gray-700 dark:bg-gray-800"
44+
>
45+
<div class="flex items-center gap-1.5 text-xs font-medium text-gray-600 dark:text-gray-300">
46+
<Keyboard :size="14" />
47+
<span class="hidden uppercase tracking-wide sm:inline">Commands</span>
48+
</div>
49+
<div class="h-4 w-px bg-gray-300 dark:bg-gray-600" />
50+
51+
<!-- Navigation -->
52+
<div class="flex items-center gap-1">
53+
<!-- Previous -->
54+
<button
55+
v-if="prevCommand"
56+
@click="prevCommand.execute"
57+
:disabled="prevCommand.disabled"
58+
class="inline-flex items-center gap-1.5 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-600 shadow-sm ring-1 ring-gray-300 transition-all hover:bg-gray-50 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-gray-700 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-600 dark:hover:text-gray-100"
59+
:title="prevCommand.shortcuts ? `Shortcut: ${prevCommand.shortcuts.join(', ')}` : undefined"
60+
>
61+
<ChevronLeft :size="14" />
62+
<kbd
63+
v-if="prevCommand.shortcuts?.[0]"
64+
class="hidden items-center rounded border border-gray-300 bg-gray-200 px-1.5 py-0.5 font-mono text-[10px] font-medium text-gray-500 sm:inline-flex dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
65+
>
66+
{{ formatShortcut(prevCommand.shortcuts[0]) }}
67+
</kbd>
68+
</button>
69+
70+
<!-- Page indicator -->
71+
<div
72+
class="min-w-[80px] rounded-md bg-white px-2 py-1 text-center shadow-sm ring-1 ring-gray-300 dark:bg-gray-700 dark:ring-gray-600"
73+
>
74+
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
75+
{{ scrollState?.currentPage }}
76+
<span class="text-gray-500 dark:text-gray-400">/</span>
77+
{{ scrollState?.totalPages }}
78+
</span>
79+
</div>
80+
81+
<!-- Next -->
82+
<button
83+
v-if="nextCommand"
84+
@click="nextCommand.execute"
85+
:disabled="nextCommand.disabled"
86+
class="inline-flex items-center gap-1.5 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-600 shadow-sm ring-1 ring-gray-300 transition-all hover:bg-gray-50 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-gray-700 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-600 dark:hover:text-gray-100"
87+
:title="nextCommand.shortcuts ? `Shortcut: ${nextCommand.shortcuts.join(', ')}` : undefined"
88+
>
89+
<ChevronRight :size="14" />
90+
<kbd
91+
v-if="nextCommand.shortcuts?.[0]"
92+
class="hidden items-center rounded border border-gray-300 bg-gray-200 px-1.5 py-0.5 font-mono text-[10px] font-medium text-gray-500 sm:inline-flex dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
93+
>
94+
{{ formatShortcut(nextCommand.shortcuts[0]) }}
95+
</kbd>
96+
</button>
97+
</div>
98+
99+
<div class="flex-1" />
100+
101+
<!-- Actions -->
102+
<button
103+
v-if="alertCommand"
104+
@click="alertCommand.execute"
105+
:disabled="alertCommand.disabled"
106+
class="inline-flex items-center gap-1.5 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-600 shadow-sm ring-1 ring-gray-300 transition-all hover:bg-gray-50 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-gray-700 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-600 dark:hover:text-gray-100"
107+
:title="alertCommand.shortcuts ? `Shortcut: ${alertCommand.shortcuts.join(', ')}` : undefined"
108+
>
109+
<Info :size="14" />
110+
<span class="hidden sm:inline">{{ alertCommand.label }}</span>
111+
<kbd
112+
v-if="alertCommand.shortcuts?.[0]"
113+
class="hidden items-center rounded border border-gray-300 bg-gray-200 px-1.5 py-0.5 font-mono text-[10px] font-medium text-gray-500 sm:inline-flex dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
114+
>
115+
{{ formatShortcut(alertCommand.shortcuts[0]) }}
116+
</kbd>
117+
</button>
118+
119+
<!-- Hint -->
120+
<span class="hidden text-[10px] text-gray-600 lg:inline dark:text-gray-300">
121+
Use keyboard shortcuts to navigate
122+
</span>
123+
</div>
124+
125+
<!-- PDF Viewer Area -->
126+
<div class="relative h-[400px] sm:h-[500px]">
127+
<Viewport :document-id="documentId" class="absolute inset-0 bg-gray-200 dark:bg-gray-800">
128+
<Scroller :document-id="documentId">
129+
<template #default="{ page }">
130+
<RenderLayer :document-id="documentId" :page-index="page.pageIndex" />
131+
</template>
132+
</Scroller>
133+
</Viewport>
134+
</div>
135+
</template>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Component from './commands-example.vue';
2+
export default Component;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<script setup lang="ts">
2+
import { usePdfiumEngine } from '@embedpdf/engines/vue';
3+
import { EmbedPDF } from '@embedpdf/core/vue';
4+
import { createPluginRegistration, type GlobalStoreState } from '@embedpdf/core';
5+
import {
6+
DocumentManagerPluginPackage,
7+
DocumentContent,
8+
} from '@embedpdf/plugin-document-manager/vue';
9+
import {
10+
ViewportPluginPackage,
11+
VIEWPORT_PLUGIN_ID,
12+
type ViewportState,
13+
} from '@embedpdf/plugin-viewport/vue';
14+
import {
15+
ScrollPluginPackage,
16+
SCROLL_PLUGIN_ID,
17+
type ScrollState,
18+
type ScrollPlugin,
19+
} from '@embedpdf/plugin-scroll/vue';
20+
import { RenderPluginPackage } from '@embedpdf/plugin-render/vue';
21+
import {
22+
ZoomPluginPackage,
23+
ZoomMode,
24+
ZOOM_PLUGIN_ID,
25+
type ZoomState,
26+
} from '@embedpdf/plugin-zoom/vue';
27+
import { CommandsPluginPackage, type Command } from '@embedpdf/plugin-commands/vue';
28+
import { Loader2 } from 'lucide-vue-next';
29+
import CommandsExampleContent from './commands-example-content.vue';
30+
31+
const { engine, isLoading } = usePdfiumEngine();
32+
33+
// Define app state type
34+
export type State = GlobalStoreState<{
35+
[ZOOM_PLUGIN_ID]: ZoomState;
36+
[VIEWPORT_PLUGIN_ID]: ViewportState;
37+
[SCROLL_PLUGIN_ID]: ScrollState;
38+
}>;
39+
40+
// Define Commands
41+
const myCommands: Record<string, Command<State>> = {
42+
'nav.prev': {
43+
id: 'nav.prev',
44+
label: 'Previous Page',
45+
shortcuts: ['arrowleft', 'k'],
46+
action: ({ registry, documentId }) => {
47+
registry
48+
.getPlugin<ScrollPlugin>('scroll')
49+
?.provides()
50+
?.forDocument(documentId)
51+
.scrollToPreviousPage();
52+
},
53+
disabled: ({ state, documentId }) => {
54+
const scrollState = state.plugins.scroll.documents[documentId];
55+
return scrollState ? scrollState.currentPage <= 1 : true;
56+
},
57+
},
58+
'nav.next': {
59+
id: 'nav.next',
60+
label: 'Next Page',
61+
shortcuts: ['arrowright', 'j'],
62+
action: ({ registry, documentId }) => {
63+
registry
64+
.getPlugin<ScrollPlugin>('scroll')
65+
?.provides()
66+
?.forDocument(documentId)
67+
.scrollToNextPage();
68+
},
69+
disabled: ({ state, documentId }) => {
70+
const scrollState = state.plugins.scroll.documents[documentId];
71+
return scrollState ? scrollState.currentPage >= scrollState.totalPages : true;
72+
},
73+
},
74+
'doc.alert': {
75+
id: 'doc.alert',
76+
label: 'Show Info',
77+
shortcuts: ['ctrl+i', 'meta+i'],
78+
action: ({ state, documentId }) => {
79+
const page = state.plugins.scroll.documents[documentId]?.currentPage;
80+
if (!page) return;
81+
alert(`You are currently reading page ${page}`);
82+
},
83+
},
84+
};
85+
86+
const plugins = [
87+
createPluginRegistration(DocumentManagerPluginPackage, {
88+
initialDocuments: [{ url: 'https://snippet.embedpdf.com/ebook.pdf' }],
89+
}),
90+
createPluginRegistration(ViewportPluginPackage),
91+
createPluginRegistration(ScrollPluginPackage),
92+
createPluginRegistration(RenderPluginPackage),
93+
createPluginRegistration(ZoomPluginPackage, {
94+
defaultZoomLevel: ZoomMode.FitPage,
95+
}),
96+
createPluginRegistration(CommandsPluginPackage, {
97+
commands: myCommands,
98+
}),
99+
];
100+
</script>
101+
102+
<template>
103+
<div
104+
v-if="isLoading || !engine"
105+
class="overflow-hidden rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-900"
106+
>
107+
<div class="flex h-[400px] items-center justify-center">
108+
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
109+
<Loader2 :size="20" class="animate-spin" />
110+
<span class="text-sm">Loading PDF Engine...</span>
111+
</div>
112+
</div>
113+
</div>
114+
<EmbedPDF v-else :engine="engine" :plugins="plugins" v-slot="{ activeDocumentId }">
115+
<DocumentContent
116+
v-if="activeDocumentId"
117+
:document-id="activeDocumentId"
118+
v-slot="{ isLoading: docLoading, isLoaded }"
119+
>
120+
<div
121+
v-if="docLoading"
122+
class="overflow-hidden rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-900"
123+
>
124+
<div class="flex h-[400px] items-center justify-center">
125+
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
126+
<Loader2 :size="20" class="animate-spin" />
127+
<span class="text-sm">Loading document...</span>
128+
</div>
129+
</div>
130+
</div>
131+
<div
132+
v-if="isLoaded"
133+
class="overflow-hidden rounded-lg border border-gray-300 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900"
134+
>
135+
<CommandsExampleContent :document-id="activeDocumentId" />
136+
</div>
137+
</DocumentContent>
138+
</EmbedPDF>
139+
</template>

0 commit comments

Comments
 (0)