Last active
May 2, 2022 02:36
-
-
Save zealot128/3491333989dd2c146142d9d77a64f806 to your computer and use it in GitHub Desktop.
vue-pdf page with selectable text: Usage: <pdf-viewer :pdf-url="pdfUrl" />
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template lang="pug"> | |
.pdf-page--wrapper(:class='{"text-selection": textSelectionEnabled }') | |
.row.mr-3 | |
.col-sm-3 | |
.col-sm-6.pdf-page--headline {{$t('js.attachment_viewer.page', { page: page })}} | |
.col-sm-3 | |
.pdf-page--buttons | |
.btn-group.mr-1(v-if="printable") | |
button.btn.btn-secondary.btn-sm(type="button" @click="print" data-toggle="tooltip" data-placement="top" :title="$t('js.attachment_viewer.print')") | |
i.mdi.mdi-printer.mdi-fw | |
.btn-group | |
button.btn.btn-sm.btn-secondary(type="button" @click.prevent='rotate += 90' data-toggle="tooltip" data-placement="top" :title="$t('js.attachment_viewer.rotate_clockwise')") | |
i.mdi.mdi-rotate-right.mdi-fw | |
button.btn.btn-sm.btn-secondary(type="button" @click.prevent='rotate -= 90' data-toggle="tooltip" data-placement="top" :title="$t('js.attachment_viewer.rotate_counterclockwise')") | |
i.mdi.mdi-rotate-left.mdi-fw | |
div(style='position: relative;') | |
div(v-if='error') | |
small {{ $t('js.recruiter.pdf_viewer.page_with_selectable_text.fehler_beim_laden') }} | |
p {{error}} | |
pdf.pdf-main( | |
ref='pdf' | |
:src='src' | |
:page='page' | |
:rotate='rotate' | |
@progress='loadedRatio = $event' | |
@page-loaded='onPageLoad' | |
@error='onError' | |
:style='{ width: zoom + "%" }' | |
) | |
div( | |
class="pdf-page--text-layer textLayer" | |
ref='textLayer' | |
:style='{width: zoom + "%" }') | |
</template> | |
<script> | |
import pdf from "vue-pdf" | |
const PDFJS = require("pdfjs-dist/es5/build/pdf.js") | |
const createTextLayer = (page, textLayer, canvas) => page.getTextContent().then(content => { | |
const rect = canvas.getBoundingClientRect() | |
textLayer.innerHTML = "" | |
// const scale = 1 | |
const scale = rect.height / page.getViewport({scale: 1}).height | |
const viewport = page.getViewport({scale}) | |
PDFJS.renderTextLayer({ | |
enhanceTextSelection: true, | |
textContent: content, | |
container: textLayer, | |
viewport, | |
}) | |
}) | |
export default { | |
components: { pdf }, | |
props: { | |
zoom: { type: Number, required: true }, | |
src: { type: Object, required: true }, | |
page: { type: Number, required: true }, | |
textSelectionEnabled: { type: Boolean, default: false }, | |
printable: { type: Boolean, default: false }, | |
printUrl: { type: String, default: () => "" }, | |
}, | |
data() { | |
return { | |
rotate: 0, | |
error: null, | |
} | |
}, | |
computed: {}, | |
watch: { | |
zoom() { | |
setTimeout(() => this.onPageLoad(), 150) | |
}, | |
}, | |
methods: { | |
print() { | |
if (this.printUrl) { | |
window.open(this.printUrl) | |
} else { | |
this.$refs.pdf.print(100) | |
} | |
}, | |
onError(error) { | |
if (typeof error === "object" && error.message) { | |
this.error = error.message | |
} | |
console.error("onError", error) | |
}, | |
onPageLoad(_pageNumber) { | |
this.$refs.pdf.pdf.forEachPage(page => { | |
if (page._pageIndex === this.page - 1) { | |
createTextLayer( | |
page, | |
this.$refs.textLayer, | |
this.$refs.pdf.$refs.canvas | |
) | |
} | |
}) | |
}, | |
}, | |
} | |
</script> | |
<style lang="scss"> | |
.pdf-page--text-layer { | |
margin: auto; | |
position: absolute; | |
left: 0; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
overflow: hidden; | |
opacity: 0.2; | |
line-height: 1; | |
::selection { | |
background-color: red !important; | |
} | |
} | |
.pdf-page--text-layer > span { | |
color: transparent; | |
position: absolute; | |
white-space: pre; | |
cursor: text; | |
transform-origin: 0% 0%; | |
} | |
.pdf-main { | |
margin: 30px auto; | |
transition: opacity 0.3s ease-in; | |
} | |
.pdf-page--buttons { | |
text-align: right; | |
margin: 5px 5px 0 0; | |
} | |
.pdf-page--headline { | |
color: #eee; | |
text-align: center; | |
align-self: center; | |
} | |
.text-selection { | |
.pdf-main { | |
opacity: 0.8; | |
} | |
.pdf-page--text-layer > div { | |
background: lighten(yellow, 30%); | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template lang="pug"> | |
div | |
.row.mb-3 | |
.col-sm-8 | |
.col-sm-4.text-right | |
.btn-toolbar.justify-content-end | |
.btn-group.mr-1(v-if="numPages > 1") | |
a.btn.btn-link.btn-outline-primary.btn-sm(role="button" v-for='number in numPages' :class='{active: number == currentPage}' @click='scrollToPage(number)') | |
| {{$t('js.attachment_viewer.page', { page: number })}} | |
.btn-group | |
button.btn.btn-sm.btn-secondary(type="button" @click.prevent='zoom += 20' data-toggle="tooltip" data-placement="top" :title="$t('js.attachment_viewer.magnify')") | |
i.mdi.mdi-magnify-plus-outline | |
button.btn.btn-sm.btn-secondary(type="button" :disabled='zoom == 40' @click.prevent='zoom -= 20' data-toggle="tooltip" data-placement="top" :title="$t('js.attachment_viewer.minimize')") | |
i.mdi.mdi-magnify-minus-outline | |
button.btn.btn-sm.btn-secondary(type="button" @click='textSelectionEnabled = !textSelectionEnabled' :class='{active: textSelectionEnabled }' data-toggle="tooltip" data-placement="top" :title="$t('js.attachment_viewer.select')") | |
i.mdi.mdi-form-textbox.mdi-fw | |
div(v-if='!numPages && !error') | |
| {{$t('js.attachment_viewer.loading')}} | |
div(v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: darkgreen; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }") | |
|{{ Math.floor(loadedRatio * 100) }}% | |
.alert.alert-danger(v-if='error') | |
small {{ $t('js.recruiter.pdf_viewer.fehler_beim_laden') }} | |
p {{error}} | |
.pdf-viewer-wrapper( | |
v-dragscroll='!textSelectionEnabled' :class='{"zoom-active": zoom > 40, "text-selection": textSelectionEnabled }' | |
style='position: relative' ref='wrapper' @scroll='onScroll' | |
) | |
page-with-selectable-text(v-for='page in numPages' :src='src' :page='page' :key='page' :zoom='zoom' :text-selection-enabled='textSelectionEnabled' ref='pdfs' :printable='printable' :printUrl='pdfUrl') | |
</template> | |
<script> | |
import { dragscroll } from "vue-dragscroll" | |
import pdf from "vue-pdf" | |
import $ from "jquery" | |
import PageWithSelectableText from "./PageWithSelectableText" | |
function checkInView(container, element, partial) { | |
const cTop = container.scrollTop | |
const cBottom = cTop + container.clientHeight | |
const eTop = element.offsetTop | |
const eBottom = eTop + element.clientHeight | |
const isTotal = eTop >= cTop && eBottom <= cBottom | |
const isPartial = | |
partial && | |
((eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom)) | |
return isTotal || isPartial | |
} | |
export default { | |
directives: { dragscroll }, | |
components: { PageWithSelectableText }, | |
props: { | |
pdfUrl: { type: String, required: true }, | |
pdfSize: { type: Number, default: null }, | |
printable: { type: Boolean, default: false } | |
}, | |
data() { | |
return { | |
textSelectionEnabled: false, | |
src: null, | |
error: null, | |
numPages: undefined, | |
show: true, | |
zoom: 40, | |
loadedRatio: 0, | |
currentPage: 1, | |
} | |
}, | |
created() { | |
const that = this | |
const loadingTask = pdf.createLoadingTask(this.pdfUrl, { | |
onProgress(status) { | |
this.loadedRatio = status.loaded / (that.pdfSize || status.total) | |
}, | |
}) | |
this.src = loadingTask | |
this.src.promise.then(loadedPdf => { | |
this.numPages = loadedPdf._pdfInfo.numPages | |
}).catch((err) => { | |
console.error("CATCH", err) | |
if (typeof err == 'object' && err.message) { | |
this.error = err.message | |
} else { | |
throw err; | |
} | |
}) | |
}, | |
mounted() { | |
this.$refs.wrapper.addEventListener("scroll", this.onScroll) | |
}, | |
destroyed() { | |
if (this.$refs.wrapper) { | |
this.$refs.wrapper.removeEventListener("scroll", this.onScroll) | |
} | |
}, | |
methods: { | |
scrollToPage(number) { | |
const offset = this.$refs.pdfs[number - 1].$el.offsetTop | |
$(this.$refs.wrapper).animate({ scrollTop: offset }) | |
}, | |
onScroll(_event) { | |
const checks = this.$refs.pdfs.find(pdfEl => { | |
const el = pdfEl.$el | |
return checkInView(this.$refs.wrapper, el, true) | |
}) | |
this.currentPage = checks.page | |
}, | |
}, | |
} | |
</script> | |
<style lang="scss"> | |
.pdf-viewer-wrapper { | |
overflow: hidden; | |
overflow-y: scroll; | |
max-height: 80vh; | |
background: #444; | |
overflow-scrolling: touch; | |
&::-webkit-scrollbar { | |
display: none; | |
} | |
&.zoom-active { | |
cursor: grab; | |
} | |
&.text-selection { | |
cursor: normal; | |
} | |
} | |
.pdf-main { | |
margin: 30px auto; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment