Selecting and Outputting Text Selected
For certain applications that require PDF interaction, you want to provide some options for your users' convenience. A more recent example is when a user select a text to ask a question in an PDF chat using AI with LLM models.
Vue PDF Viewer supports additional actions when selecting text, allowing for an interactive experience with PDF pages.
This tutorial will guide you through how to show a popover of options (i.e. Ask and Copy) after selecting text on a PDF page.
Here is a quick overview of what you’ll learn in this tutorial:
- Detect and capture selected text inside a PDF viewer
- Show a floating popover menu near the selection
- Provide actions like Ask and Copy for the selected text
Components Overview
1. VPdfViewer
- The core PDF viewer component from
@vue-pdf-viewer/viewer
. - This renders the PDF content and allows interaction such as text selection inside a PDF page.
2. Popover Menu
- A small floating popover showing actions like
Ask
andCopy
when text is selected. - The popover menu appears next to the selected text using dynamic positioning for user-friendly experience.
Code Explanation
Script
- When the component is mounted, it sets up an observer to wait until the PDF content is ready.
- A
mouseup
event listener is added to detect when a user selects some text inside the PDF. - If text is selected, it calculates the position and shows a popover menu nearby.
- The popover menu provides two actions, namely
Ask
the selected text orCopy
the selection.
vue
<script lang="ts" setup>
// Import the VPdfViewer component from vue-pdf-viewer
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
// Import Vue composition functions
import { onMounted, ref } from "vue";
// Reference to the PDF viewer component instance
const vpvRef = ref<InstanceType<typeof VPdfViewer>>();
// Boolean to control the visibility of the dropdown menu
const showDropdown = ref(false);
// Store the position of the dropdown menu
const menuPosition = ref({ x: 0, y: 0 });
// Store the selected text from the PDF
const selectedText = ref<string>();
// Store the selected ask text from the PDF
const selectedAskText = ref<string>();
// Function to copy the selected text to the clipboard
const handleCopy = () => {
if (selectedText.value) {
// Use Clipboard API to write selected text
navigator.clipboard.writeText(selectedText.value);
}
// Clear selection and hide dropdown
clearSelection();
};
// Function to ask the selected text
const handleAsk = () => {
if (selectedText.value) {
// Set into selected ask text value
selectedAskText.value = selectedText.value;
}
// Clear selection and hide dropdown
clearSelection();
};
// Function to clear current selection and hide dropdown
const clearSelection = () => {
// Remove text selection from the window
window.getSelection()?.removeAllRanges();
showDropdown.value = false;
selectedText.value = undefined;
};
// Run when component is mounted
onMounted(() => {
// Create a mutation observer to wait until PDF content is loaded
const observer = new MutationObserver(() => {
const element = vpvRef.value?.$el; // Get the element of the PDF viewer
if (!element) {
return;
}
// Add mouseup event listener to detect text selection
element.addEventListener("mouseup", () => {
vpvRef.value?.$el.addEventListener("mouseup", () => {
const selection = window.getSelection(); // Get current selection
const selectedString = selection?.toString(); // Convert to string
const selectedRange = selection?.getRangeAt(0); // Get the range object
// Set selected text so we can use it later
selectedText.value = selectedString;
// If there's valid selection, show dropdown
if (
selectedString &&
selectedString.trim().length > 0 &&
selectedRange
) {
const rangeBounds = selectedRange.getBoundingClientRect();
// Position the dropdown near the selection
menuPosition.value = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
showDropdown.value = true;
} else {
showDropdown.value = false;
}
});
});
// Stop observing once setup is complete
observer.disconnect();
});
// Start observing changes to the body (useful for dynamic loading)
observer.observe(document.body, { childList: true, subtree: true });
});
</script>
vue
<script setup>
// Import the VPdfViewer component from vue-pdf-viewer
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
// Import Vue composition functions
import { onMounted, ref } from "vue";
// Reference to the PDF viewer component instance
const vpvRef = ref();
// Boolean to control the visibility of the dropdown menu
const showDropdown = ref(false);
// Store the position of the dropdown menu
const menuPosition = ref({ x: 0, y: 0 });
// Store the selected text from the PDF
const selectedText = ref();
// Store the selected ask text from the PDF
const selectedAskText = ref();
// Function to copy the selected text to the clipboard
const handleCopy = () => {
if (selectedText.value) {
// Use Clipboard API to write selected text
navigator.clipboard.writeText(selectedText.value);
}
// Clear selection and hide dropdown
clearSelection();
};
// Function to ask the selected text
const handleAsk = () => {
if (selectedText.value) {
// Set into selected ask text value
selectedAskText.value = selectedText.value;
}
// Clear selection and hide dropdown
clearSelection();
};
// Function to clear current selection and hide dropdown
const clearSelection = () => {
// Remove text selection from the window
window.getSelection()?.removeAllRanges();
showDropdown.value = false;
selectedText.value = undefined;
};
// Run when component is mounted
onMounted(() => {
// Create a mutation observer to wait until PDF content is loaded
const observer = new MutationObserver(() => {
const element = vpvRef.value?.$el;
if (!element) return;
// Add mouseup event listener to detect text selection
element.addEventListener("mouseup", () => {
const selection = window.getSelection(); // Get current selection
const selectedString = selection?.toString(); // Convert to string
const selectedRange = selection?.getRangeAt(0); // Get the range object
// Set selected text so we can use it later
selectedText.value = selectedString;
// If there's valid selection, show dropdown
if (selectedString && selectedString.trim().length > 0 && selectedRange) {
const rangeBounds = selectedRange.getBoundingClientRect();
// Position the dropdown near the selection
menuPosition.value = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
showDropdown.value = true;
} else {
showDropdown.value = false;
}
});
// Stop observing once setup is complete
observer.disconnect();
});
// Start observing changes to the body (useful for dynamic loading)
observer.observe(document.body, { childList: true, subtree: true });
});
</script>
vue
<script lang="ts">
// Import the VPdfViewer component from vue-pdf-viewer
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
// Import Vue composition functions
import { defineComponent } from "vue";
export default defineComponent({
components: { VPdfViewer },
data() {
return {
showDropdown: false,
menuPosition: { x: 0, y: 0 },
selectedText: undefined as string | undefined,
selectedAskText: undefined as string | undefined,
};
},
// Run when component is mounted
mounted() {
// Create a mutation observer to wait until PDF content is loaded
const observer = new MutationObserver(() => {
const element = (this.$refs.vpvRef as any)?.$el;
if (!element) return;
// Add mouseup event listener to detect text selection
element.addEventListener("mouseup", () => {
const selection = window.getSelection(); // Get current selection
const selectedString = selection?.toString(); // Convert to string
const selectedRange = selection?.getRangeAt(0); // Get the range object
// Set selected text so we can use it later
this.selectedText = selectedString;
// If there's valid selection, show dropdown
if (
selectedString &&
selectedString.trim().length > 0 &&
selectedRange
) {
const rangeBounds = selectedRange.getBoundingClientRect();
// Position the dropdown near the selection
this.menuPosition = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
this.showDropdown = true;
} else {
this.showDropdown = false;
}
});
// Stop observing once setup is complete
observer.disconnect();
});
// Start observing changes to the body (useful for dynamic loading)
observer.observe(document.body, { childList: true, subtree: true });
},
methods: {
// Function to copy the selected text to the clipboard
handleCopy() {
if (this.selectedText) {
// Use Clipboard API to write selected text
navigator.clipboard.writeText(this.selectedText);
}
// Clear selection and hide dropdown
this.clearSelection();
},
// Function to ask the selected text
handleAsk() {
if (this.selectedText) {
// Set into selected ask text value
this.selectedAskText = this.selectedText;
}
// Clear selection and hide dropdown
this.clearSelection();
},
// Function to clear current selection and hide dropdown
clearSelection() {
// Remove text selection from the window
window.getSelection()?.removeAllRanges();
this.showDropdown = false;
this.selectedText = undefined;
},
},
});
</script>
vue
<script>
// Import the VPdfViewer component from vue-pdf-viewer
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
export default {
components: { VPdfViewer },
data() {
return {
vpvRef: null,
showDropdown: false,
menuPosition: { x: 0, y: 0 },
selectedText: undefined,
selectedAskText: undefined,
};
},
// Run when component is mounted
mounted() {
// Create a mutation observer to wait until PDF content is loaded
const observer = new MutationObserver(() => {
const element = this.$refs.vpvRef?.$el;
if (!element) return;
// Add mouseup event listener to detect text selection
element.addEventListener("mouseup", () => {
const selection = window.getSelection(); // Get current selection
const selectedString = selection?.toString(); // Convert to string
const selectedRange = selection?.getRangeAt(0); // Get the range object
// Set selected text so we can use it later
this.selectedText = selectedString;
// If there's valid selection, show dropdown
if (
selectedString &&
selectedString.trim().length > 0 &&
selectedRange
) {
const rangeBounds = selectedRange.getBoundingClientRect();
// Position the dropdown near the selection
this.menuPosition = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
this.showDropdown = true;
} else {
this.showDropdown = false;
}
});
// Stop observing once setup is complete
observer.disconnect();
});
// Start observing changes to the body (useful for dynamic loading)
observer.observe(document.body, { childList: true, subtree: true });
},
methods: {
// Function to copy the selected text to the clipboard
handleCopy() {
if (this.selectedText) {
// Use Clipboard API to write selected text
navigator.clipboard.writeText(this.selectedText);
}
// Clear selection and hide dropdown
this.clearSelection();
},
// Function to ask the selected text
handleAsk() {
if (this.selectedText) {
// Set into selected ask text value
this.selectedAskText = this.selectedText;
}
// Clear selection and hide dropdown
this.clearSelection();
},
// Function to clear current selection and hide dropdown
clearSelection() {
// Remove text selection from the window
window.getSelection()?.removeAllRanges();
this.showDropdown = false;
this.selectedText = undefined;
},
},
};
</script>
Template
- Renders the
VPdfViewer
component inside a styled container. - Shows the popover menu dynamically if text is selected.
- Positions the menu based on where the selection occurs.
- Shows the
Ask
card of the selected text
vue
<template>
<!-- Main container -->
<div :style="{ display: 'flex', padding: '8px' }">
<!-- Left panel: container for the PDF viewer -->
<div :style="{ width: '1028px', height: '700px', position: 'relative' }">
<!-- Render the PDF using VPdfViewer -->
<VPdfViewer
ref="vpvRef"
src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
/>
</div>
<!-- Right panel: card UI for showing selected asked text -->
<div class="card">
<!-- Card header -->
<div class="card-header">
Ask (after highlight a text and click "Ask")
</div>
<!-- Divider line between header and content -->
<div class="card-divider" />
<!-- Card content showing the selected text that will be asked -->
<div class="card-content">
<!-- Display the selected text from the PDF -->
{{ selectedAskText }}
</div>
</div>
</div>
<!-- Floating Dropdown Menu when text is selected -->
<div
v-if="showDropdown"
class="dropdown-menu"
:style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }"
>
<ul>
<!-- Option to ask the selected text -->
<li @click="handleAsk">Ask</li>
<!-- Option to copy the selected text -->
<li @click="handleCopy">Copy</li>
</ul>
</div>
</template>
Styles
- Style the card and the popover menu to look clean and simple
- Add a hover effect to make the options visually interactive
css
<style lang="css" scoped>
/* Card container */
.card {
width: 360px;
height: fit-content;
background-color: #fff;
border-radius: 8px;
margin-left: 12px;
font-family: sans-serif;
border: 1px solid rgba(0, 0, 0, 0.1);
}
/* Header section of the card */
.card-header {
padding: 16px;
font-weight: bold;
}
/* Divider line between card sections */
.card-divider {
height: 1px;
background-color: #e5e7eb;
}
/* Scrollable content area inside the card */
.card-content {
height: 600px;
padding: 16px;
line-height: 1.5;
overflow: auto;
}
/* Container for the dropdown menu */
.dropdown-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
z-index: 100;
}
/* Horizontal list inside the dropdown menu */
.dropdown-menu ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
/* Hover state for dropdown items */
.dropdown-menu li:hover {
background-color: #efefef;
}
/* Individual dropdown menu item */
.dropdown-menu li {
padding: 8px 12px;
cursor: pointer;
color: #000;
text-align: center;
white-space: nowrap;
}
/* Divider between dropdown items */
.dropdown-menu li + li {
border-left: 1px solid #ccc;
}
</style>
Complete example
Here’s how everything fits together:
- The
VPdfViewer
loads and displays the PDF. - When a user selects some text on a PDF page, a popover menu appears.
- The popover menu gives the user options to ask or copy the selection.
Here is a complete example of how you can display options after selection some text on Vue PDF Viewer.
vue
<script lang="ts" setup>
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
import { onMounted, ref } from "vue";
const vpvRef = ref<InstanceType<typeof VPdfViewer>>();
const showDropdown = ref(false);
const menuPosition = ref({ x: 0, y: 0 });
const selectedText = ref<string>();
const selectedAskText = ref<string>();
const handleCopy = () => {
if (selectedText.value) {
navigator.clipboard.writeText(selectedText.value);
}
clearSelection();
};
const handleAsk = () => {
if (selectedText.value) {
selectedAskText.value = selectedText.value;
}
clearSelection();
};
const clearSelection = () => {
window.getSelection()?.removeAllRanges();
showDropdown.value = false;
selectedText.value = undefined;
};
onMounted(() => {
const observer = new MutationObserver(() => {
const element = vpvRef.value?.$el;
if (!element) {
return;
}
element.addEventListener("mouseup", () => {
vpvRef.value?.$el.addEventListener("mouseup", () => {
const selection = window.getSelection();
const selectedString = selection?.toString();
const selectedRange = selection?.getRangeAt(0);
selectedText.value = selectedString;
if (
selectedString &&
selectedString.trim().length > 0 &&
selectedRange
) {
const rangeBounds = selectedRange.getBoundingClientRect();
menuPosition.value = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
showDropdown.value = true;
} else {
showDropdown.value = false;
}
});
});
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
});
</script>
<template>
<div :style="{ display: 'flex', padding: '8px' }">
<div :style="{ width: '1028px', height: '700px', position: 'relative' }">
<VPdfViewer
ref="vpvRef"
src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
/>
</div>
<div class="card">
<div class="card-header">
Ask (after highlight a text and click "Ask")
</div>
<div class="card-divider" />
<div class="card-content">
{{ selectedAskText }}
</div>
</div>
</div>
<div
v-if="showDropdown"
class="dropdown-menu"
:style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }"
>
<ul>
<li @click="handleAsk">Ask</li>
<li @click="handleCopy">Copy</li>
</ul>
</div>
</template>
<style lang="css" scoped>
.card {
width: 360px;
height: fit-content;
background-color: #fff;
border-radius: 8px;
margin-left: 12px;
font-family: sans-serif;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 16px;
font-weight: bold;
}
.card-divider {
height: 1px;
background-color: #e5e7eb;
}
.card-content {
height: 600px;
padding: 16px;
line-height: 1.5;
overflow: auto;
}
.dropdown-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
z-index: 100;
}
.dropdown-menu ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
.dropdown-menu li:hover {
background-color: #efefef;
}
.dropdown-menu li {
padding: 8px 12px;
cursor: pointer;
color: #000;
text-align: center;
white-space: nowrap;
}
.dropdown-menu li + li {
border-left: 1px solid #ccc;
}
</style>
vue
<script>
import { defineComponent, onMounted, ref } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
export default defineComponent({
components: { VPdfViewer },
setup() {
const vpvRef = ref();
const showDropdown = ref(false);
const menuPosition = ref({ x: 0, y: 0 });
const selectedText = ref();
const selectedAskText = ref();
const handleCopy = () => {
if (selectedText.value) {
navigator.clipboard.writeText(selectedText.value);
}
clearSelection();
};
const handleAsk = () => {
if (selectedText.value) {
selectedAskText.value = selectedText.value;
}
clearSelection();
};
const clearSelection = () => {
window.getSelection()?.removeAllRanges();
showDropdown.value = false;
selectedText.value = undefined;
};
onMounted(() => {
const observer = new MutationObserver(() => {
const element = vpvRef.value?.$el;
if (!element) return;
element.addEventListener("mouseup", () => {
const selection = window.getSelection();
const selectedString = selection?.toString();
const selectedRange = selection?.getRangeAt(0);
selectedText.value = selectedString;
if (
selectedString &&
selectedString.trim().length > 0 &&
selectedRange
) {
const rangeBounds = selectedRange.getBoundingClientRect();
menuPosition.value = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
showDropdown.value = true;
} else {
showDropdown.value = false;
}
});
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
});
return {
vpvRef,
showDropdown,
menuPosition,
selectedText,
selectedAskText,
handleCopy,
handleAsk,
};
},
});
</script>
<template>
<div :style="{ display: 'flex', padding: '8px' }">
<div :style="{ width: '1028px', height: '700px', position: 'relative' }">
<VPdfViewer
ref="vpvRef"
src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
/>
</div>
<div class="card">
<div class="card-header">
Ask (after highlight a text and click "Ask")
</div>
<div class="card-divider" />
<div class="card-content">
{{ selectedAskText }}
</div>
</div>
</div>
<div
v-if="showDropdown"
class="dropdown-menu"
:style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }"
>
<ul>
<li @click="handleAsk">Ask</li>
<li @click="handleCopy">Copy</li>
</ul>
</div>
</template>
<style lang="css" scoped>
.card {
width: 360px;
height: fit-content;
background-color: #fff;
border-radius: 8px;
margin-left: 12px;
font-family: sans-serif;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 16px;
font-weight: bold;
}
.card-divider {
height: 1px;
background-color: #e5e7eb;
}
.card-content {
height: 600px;
padding: 16px;
line-height: 1.5;
overflow: auto;
}
.dropdown-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
z-index: 100;
}
.dropdown-menu ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
.dropdown-menu li:hover {
background-color: #efefef;
}
.dropdown-menu li {
padding: 8px 12px;
cursor: pointer;
color: #000;
text-align: center;
white-space: nowrap;
}
.dropdown-menu li + li {
border-left: 1px solid #ccc;
}
</style>
vue
<script lang="ts">
import { defineComponent } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
export default defineComponent({
components: { VPdfViewer },
data() {
return {
vpvRef: null as InstanceType<typeof VPdfViewer> | null,
showDropdown: false,
menuPosition: { x: 0, y: 0 },
selectedText: undefined as string | undefined,
selectedAskText: undefined as string | undefined,
};
},
methods: {
handleCopy() {
if (this.selectedText) {
navigator.clipboard.writeText(this.selectedText);
}
this.clearSelection();
},
handleAsk() {
if (this.selectedText) {
this.selectedAskText = this.selectedText;
}
this.clearSelection();
},
clearSelection() {
window.getSelection()?.removeAllRanges();
this.showDropdown = false;
this.selectedText = undefined;
},
},
mounted() {
const observer = new MutationObserver(() => {
const element = (this.$refs.vpvRef as any)?.$el;
if (!element) return;
element.addEventListener("mouseup", () => {
const selection = window.getSelection();
const selectedString = selection?.toString();
const selectedRange = selection?.getRangeAt(0);
this.selectedText = selectedString;
if (
selectedString &&
selectedString.trim().length > 0 &&
selectedRange
) {
const rangeBounds = selectedRange.getBoundingClientRect();
this.menuPosition = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
this.showDropdown = true;
} else {
this.showDropdown = false;
}
});
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
},
});
</script>
<template>
<div :style="{ display: 'flex', padding: '8px' }">
<div :style="{ width: '1028px', height: '700px', position: 'relative' }">
<VPdfViewer
ref="vpvRef"
src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
/>
</div>
<div class="card">
<div class="card-header">
Ask (after highlight a text and click "Ask")
</div>
<div class="card-divider" />
<div class="card-content">
{{ selectedAskText }}
</div>
</div>
</div>
<div
v-if="showDropdown"
class="dropdown-menu"
:style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }"
>
<ul>
<li @click="handleAsk">Ask</li>
<li @click="handleCopy">Copy</li>
</ul>
</div>
</template>
<style lang="css" scoped>
.card {
width: 360px;
height: fit-content;
background-color: #fff;
border-radius: 8px;
margin-left: 12px;
font-family: sans-serif;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 16px;
font-weight: bold;
}
.card-divider {
height: 1px;
background-color: #e5e7eb;
}
.card-content {
height: 600px;
padding: 16px;
line-height: 1.5;
overflow: auto;
}
.dropdown-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
z-index: 100;
}
.dropdown-menu ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
.dropdown-menu li:hover {
background-color: #efefef;
}
.dropdown-menu li {
padding: 8px 12px;
cursor: pointer;
color: #000;
text-align: center;
white-space: nowrap;
}
.dropdown-menu li + li {
border-left: 1px solid #ccc;
}
</style>
vue
<script>
import { VPdfViewer } from "@vue-pdf-viewer/viewer";
export default {
components: { VPdfViewer },
data() {
return {
vpvRef: null,
showDropdown: false,
menuPosition: { x: 0, y: 0 },
selectedText: undefined,
selectedAskText: undefined,
};
},
methods: {
handleCopy() {
if (this.selectedText) {
navigator.clipboard.writeText(this.selectedText);
}
this.clearSelection();
},
handleAsk() {
if (this.selectedText) {
this.selectedAskText = this.selectedText;
}
this.clearSelection();
},
clearSelection() {
window.getSelection()?.removeAllRanges();
this.showDropdown = false;
this.selectedText = undefined;
},
},
mounted() {
const observer = new MutationObserver(() => {
const element = this.$refs.vpvRef?.$el;
if (!element) return;
element.addEventListener("mouseup", () => {
const selection = window.getSelection();
const selectedString = selection?.toString();
const selectedRange = selection?.getRangeAt(0);
this.selectedText = selectedString;
if (
selectedString &&
selectedString.trim().length > 0 &&
selectedRange
) {
const rangeBounds = selectedRange.getBoundingClientRect();
this.menuPosition = {
x: rangeBounds.left + window.scrollX,
y: rangeBounds.bottom + window.scrollY,
};
this.showDropdown = true;
} else {
this.showDropdown = false;
}
});
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
},
};
</script>
<template>
<div :style="{ display: 'flex', padding: '8px' }">
<div :style="{ width: '1028px', height: '700px', position: 'relative' }">
<VPdfViewer
ref="vpvRef"
src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
/>
</div>
<div class="card">
<div class="card-header">
Ask (after highlight a text and click "Ask")
</div>
<div class="card-divider" />
<div class="card-content">
{{ selectedAskText }}
</div>
</div>
</div>
<div
v-if="showDropdown"
class="dropdown-menu"
:style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }"
>
<ul>
<li @click="handleAsk">Ask</li>
<li @click="handleCopy">Copy</li>
</ul>
</div>
</template>
<style lang="css" scoped>
.card {
width: 360px;
height: fit-content;
background-color: #fff;
border-radius: 8px;
margin-left: 12px;
font-family: sans-serif;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 16px;
font-weight: bold;
}
.card-divider {
height: 1px;
background-color: #e5e7eb;
}
.card-content {
height: 600px;
padding: 16px;
line-height: 1.5;
overflow: auto;
}
.dropdown-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
z-index: 100;
}
.dropdown-menu ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
.dropdown-menu li:hover {
background-color: #efefef;
}
.dropdown-menu li {
padding: 8px 12px;
cursor: pointer;
color: #000;
text-align: center;
white-space: nowrap;
}
.dropdown-menu li + li {
border-left: 1px solid #ccc;
}
</style>