Skip to content

Instantly share code, notes, and snippets.

@zealot128
Last active May 2, 2022 02:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zealot128/3491333989dd2c146142d9d77a64f806 to your computer and use it in GitHub Desktop.
Save zealot128/3491333989dd2c146142d9d77a64f806 to your computer and use it in GitHub Desktop.
vue-pdf page with selectable text: Usage: <pdf-viewer :pdf-url="pdfUrl" />
<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>
<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