362 lines
15 KiB
JavaScript
362 lines
15 KiB
JavaScript
Vue.component('tt-fullscreen-viewer', {
|
|
props: {
|
|
item: { type: Object, required: true },
|
|
url: { type: String, default: null },
|
|
items: { type: Array, default: () => [] },
|
|
initialIndex: { type: Number, default: -1 },
|
|
},
|
|
data: () => ({
|
|
currentItem: null,
|
|
currentImageIndex: -1,
|
|
zoom: 1,
|
|
pan: { x: 0, y: 0 },
|
|
isPanning: false,
|
|
panStart: { x: 0, y: 0 },
|
|
lastPinchDist: 0,
|
|
isLoading: true,
|
|
isStandalone: false,
|
|
}),
|
|
computed: {
|
|
contentSrc() {
|
|
if (this.url) {
|
|
return this.url;
|
|
}
|
|
if (this.currentItem?.fileId) {
|
|
return `/File/show?id=${this.currentItem.fileId}`;
|
|
}
|
|
return null;
|
|
},
|
|
isViewingImage() {
|
|
return this.currentItem && this.isImage(this.currentItem);
|
|
},
|
|
imageTransformStyle() {
|
|
return {
|
|
transform: `translate(${this.pan.x}px, ${this.pan.y}px) scale(${this.zoom})`,
|
|
cursor: this.isPanning ? 'grabbing' : 'grab',
|
|
transition: this.isPanning ? 'none' : 'transform 0.2s',
|
|
};
|
|
},
|
|
downloadUrl() {
|
|
return this.currentItem?.fileId ? `/File/download?id=${this.currentItem.fileId}` : '#';
|
|
}
|
|
},
|
|
// NEW: Watcher to trigger PDF.js rendering when item changes in standalone mode
|
|
watch: {
|
|
currentItem(newItem) {
|
|
if (this.isPdf(newItem) && this.isStandalone) {
|
|
this.$nextTick(() => {
|
|
this.renderPdfWithJs(this.contentSrc);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
isImage: file => file?.mimetype?.startsWith('image/'),
|
|
isPdf: file => file?.mimetype === 'application/pdf',
|
|
onContentLoad() {
|
|
this.isLoading = false;
|
|
},
|
|
closeViewer() {
|
|
this.$emit('close');
|
|
},
|
|
navigateImage(direction) {
|
|
const newIndex = this.currentImageIndex + direction;
|
|
if (newIndex >= 0 && newIndex < this.items.length) {
|
|
this.isLoading = true;
|
|
this.currentImageIndex = newIndex;
|
|
this.currentItem = this.items[newIndex];
|
|
this.resetZoomAndPan();
|
|
}
|
|
},
|
|
handleKeyDown(e) {
|
|
e.stopPropagation();
|
|
if (!this.currentItem) return;
|
|
switch (e.key) {
|
|
case 'Escape': this.closeViewer(); break;
|
|
case 'ArrowLeft': this.isViewingImage && this.navigateImage(-1); break;
|
|
case 'ArrowRight': this.isViewingImage && this.navigateImage(1); break;
|
|
}
|
|
},
|
|
resetZoomAndPan() {
|
|
this.zoom = 1;
|
|
this.pan = { x: 0, y: 0 };
|
|
this.isPanning = false;
|
|
},
|
|
handleWheel(e) {
|
|
if (!this.isViewingImage) return;
|
|
e.preventDefault();
|
|
const scaleFactor = e.deltaY > 0 ? -0.2 : 0.2;
|
|
this.zoom = Math.max(1, Math.min(this.zoom + scaleFactor, 5));
|
|
},
|
|
onPanStart(e) {
|
|
if (this.zoom <= 1) return;
|
|
e.preventDefault();
|
|
this.isPanning = true;
|
|
this.panStart.x = e.clientX - this.pan.x;
|
|
this.panStart.y = e.clientY - this.pan.y;
|
|
},
|
|
onPanMove(e) {
|
|
if (!this.isPanning) return;
|
|
this.pan.x = e.clientX - this.panStart.x;
|
|
this.pan.y = e.clientY - this.panStart.y;
|
|
},
|
|
onPanEnd() {
|
|
this.isPanning = false;
|
|
},
|
|
onTouchStart(e) {},
|
|
onTouchMove(e) {},
|
|
onTouchEnd(e) {},
|
|
|
|
// NEW: Method to dynamically load the PDF.js library
|
|
loadPdfJsScript() {
|
|
return new Promise((resolve, reject) => {
|
|
if (document.getElementById('pdfjs-script')) {
|
|
// Script tag exists, check if library is loaded
|
|
if (window.pdfjsLib) {
|
|
resolve();
|
|
} else {
|
|
// Tag exists but script not loaded yet, wait for it
|
|
document.getElementById('pdfjs-script').addEventListener('load', () => resolve());
|
|
document.getElementById('pdfjs-script').addEventListener('error', (e) => reject(e));
|
|
}
|
|
return;
|
|
}
|
|
|
|
const script = document.createElement('script');
|
|
script.id = 'pdfjs-script';
|
|
script.type = 'module';
|
|
script.src = 'https://unpkg.com/pdfjs-dist@5.4.54/build/pdf.mjs'; // CDN URL
|
|
script.onload = () => resolve();
|
|
script.onerror = (err) => {
|
|
console.error("Failed to load PDF.js script.", err);
|
|
reject(new Error("PDF.js script could not be loaded."));
|
|
};
|
|
document.head.appendChild(script);
|
|
});
|
|
},
|
|
|
|
// NEW: Method to render PDF onto a canvas using PDF.js
|
|
async renderPdfWithJs(url) {
|
|
if (!url) return;
|
|
this.isLoading = true;
|
|
|
|
try {
|
|
await this.loadPdfJsScript();
|
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@5.4.54/build/pdf.worker.min.mjs';
|
|
|
|
const pdf = await pdfjsLib.getDocument(url).promise;
|
|
// For simplicity, we render the first page. Add pagination controls as needed.
|
|
const page = await pdf.getPage(1);
|
|
|
|
const canvas = this.$refs.pdfCanvas;
|
|
if (!canvas) return; // Exit if canvas is not available
|
|
const container = canvas.parentElement; // Get the container to calculate size from
|
|
if (!container) return;
|
|
|
|
const context = canvas.getContext('2d');
|
|
const unscaledViewport = page.getViewport({ scale: 1 });
|
|
const scale = Math.min(
|
|
container.clientWidth / unscaledViewport.width,
|
|
container.clientHeight / unscaledViewport.height
|
|
);
|
|
|
|
const viewport = page.getViewport({ scale: scale });
|
|
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
|
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
|
|
|
} catch (error) {
|
|
console.error('Error rendering PDF with PDF.js:', error);
|
|
// Optionally show an error message to the user
|
|
} finally {
|
|
this.onContentLoad(); // Use existing method to hide loader
|
|
}
|
|
},
|
|
},
|
|
created() {
|
|
this.currentItem = this.item;
|
|
this.currentImageIndex = this.initialIndex;
|
|
|
|
// NEW: Check for standalone mode on component creation
|
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
this.isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
|
}
|
|
},
|
|
mounted() {
|
|
document.body.style.overflow = 'hidden';
|
|
this.$nextTick(() => {
|
|
this.$refs.viewer?.focus();
|
|
|
|
// NEW: Trigger initial PDF render on mount if in standalone mode
|
|
if (this.isPdf(this.currentItem) && this.isStandalone) {
|
|
this.renderPdfWithJs(this.contentSrc);
|
|
}
|
|
});
|
|
},
|
|
beforeDestroy() {
|
|
document.body.style.overflow = '';
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div class="tt-fullscreen-overlay" @click.self="closeViewer" @keydown="handleKeyDown" tabindex="-1" ref="viewer">
|
|
<div class="tt-fullscreen-toolbar">
|
|
<a v-if="currentItem.fileId" :href="downloadUrl" download class="tt-fullscreen-btn" title="Download">
|
|
<i class="fas fa-download"></i>
|
|
</a>
|
|
<button class="tt-fullscreen-btn" @click="closeViewer" title="Close"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
|
|
<div class="tt-fullscreen-content" @click.self="closeViewer">
|
|
<div v-if="isLoading" class="tt-fullscreen-loader">
|
|
<svg version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve" style="width: 80px; height: 80px;">
|
|
<path fill="#fff" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"></path>
|
|
</svg>
|
|
</div>
|
|
|
|
<template v-if="isViewingImage">
|
|
<div class="tt-fullscreen-image-wrapper" @wheel="handleWheel" @mousedown="onPanStart" @mousemove="onPanMove"
|
|
@mouseup="onPanEnd" @mouseleave="onPanEnd">
|
|
<img v-show="!isLoading" :src="contentSrc" class="tt-fullscreen-image" :style="imageTransformStyle"
|
|
@load="onContentLoad" @error="onContentLoad" @click.stop/>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else-if="isPdf(currentItem) && isStandalone" v-show="!isLoading" class="tt-fullscreen-pdf-standalone-container">
|
|
<canvas ref="pdfCanvas" class="tt-fullscreen-pdf-standalone"></canvas>
|
|
</div>
|
|
|
|
<iframe v-else-if="isPdf(currentItem)" v-show="!isLoading" :src="contentSrc" class="tt-fullscreen-pdf"
|
|
@load="onContentLoad" @error="onContentLoad" @click.stop></iframe>
|
|
</div>
|
|
|
|
<template v-if="isViewingImage && items.length > 1">
|
|
<button class="tt-fullscreen-nav-btn left" @click.stop="navigateImage(-1)" v-if="currentImageIndex > 0">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</button>
|
|
<button class="tt-fullscreen-nav-btn right" @click.stop="navigateImage(1)" v-if="currentImageIndex < items.length - 1">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
</template>
|
|
</div>`,
|
|
})
|
|
|
|
Vue.component('tt-file-gallery', {
|
|
props: {
|
|
files: {type: Array, default: () => []},
|
|
editMode: {type: Boolean, default: false},
|
|
deleteMode: {type: Boolean, default: false},
|
|
selectable: {type: Boolean, default: false},
|
|
},
|
|
data: () => ({
|
|
fullscreenItem: null,
|
|
editingFile: null,
|
|
selectedFiles: [],
|
|
}),
|
|
computed: {
|
|
imageFiles() {
|
|
return this.files.filter(this.isImage);
|
|
},
|
|
},
|
|
methods: {
|
|
isImage: file => file.mimetype?.startsWith('image/'),
|
|
isPdf: file => file.mimetype === 'application/pdf',
|
|
|
|
getFileIcon(file) {
|
|
const extension = file.fileName?.split('.').pop().toLowerCase();
|
|
switch (extension) {
|
|
case 'doc':
|
|
case 'docx':
|
|
return 'fas fa-file-word text-primary';
|
|
case 'xls':
|
|
case 'xlsx':
|
|
return 'fas fa-file-excel text-success';
|
|
case 'zip':
|
|
case 'rar':
|
|
return 'fas fa-file-archive text-warning';
|
|
default:
|
|
return 'fas fa-file text-secondary';
|
|
}
|
|
},
|
|
|
|
toggleSelection(fileId) {
|
|
if (!this.selectable) return;
|
|
const index = this.selectedFiles.indexOf(fileId);
|
|
if (index > -1) this.selectedFiles.splice(index, 1);
|
|
else this.selectedFiles.push(fileId);
|
|
this.$emit('selection-changed', this.selectedFiles);
|
|
},
|
|
|
|
openViewer(file) {
|
|
if (this.editingFile) return;
|
|
this.fullscreenItem = file;
|
|
},
|
|
closeViewer() {
|
|
this.fullscreenItem = null;
|
|
},
|
|
|
|
startEdit(file, event) {
|
|
event?.stopPropagation();
|
|
this.editingFile = {...file};
|
|
},
|
|
cancelEdit() {
|
|
this.editingFile = null;
|
|
},
|
|
saveEdit(event) {
|
|
event?.stopPropagation();
|
|
this.$emit('update-file', this.editingFile);
|
|
this.editingFile = null;
|
|
},
|
|
deleteFile(file, event) {
|
|
event?.stopPropagation();
|
|
if (confirm(`Sind Sie sicher, dass Sie die Datei "${file.fileName}" löschen möchten?`)) {
|
|
this.$emit('delete-file', file);
|
|
}
|
|
},
|
|
},
|
|
template: `
|
|
<div class="card">
|
|
<div class="card-header"><i class="fas fa-images mr-2"></i><h5>Hochgeladene Dokumente</h5></div>
|
|
<div v-if="!files.length" class="card-body text-center text-muted">Keine Dokumente vorhanden.</div>
|
|
<div v-else class="card-body">
|
|
<div class="tt-file-gallery-grid">
|
|
<div v-for="file in files" :key="file.id" class="tt-file-gallery-item"
|
|
:class="[{ 'selected': selectable && selectedFiles.includes(file.id) }, file.class]"
|
|
@click="openViewer(file)">
|
|
<div v-if="selectable" class="selection-indicator" @click.stop="toggleSelection(file.id)">
|
|
<i :class="selectedFiles.includes(file.id) ? 'fas fa-check-circle text-primary' : 'far fa-circle text-muted'"></i>
|
|
</div>
|
|
<div class="tt-file-gallery-thumbnail-wrapper">
|
|
<img v-if="isImage(file)" :src="'/File/show?id=' + file.fileId + '&size=small'"
|
|
class="tt-file-gallery-thumbnail" :alt="file.fileName">
|
|
<div v-else-if="isPdf(file)" class="tt-file-gallery-icon-container"><i
|
|
class="fas fa-file-pdf fa-3x text-danger"></i></div>
|
|
<a v-else :href="'/File/download?id=' + file.fileId" target="_blank" @click.stop
|
|
class="tt-file-gallery-icon-container">
|
|
<i :class="getFileIcon(file)" class="fa-3x"></i>
|
|
</a>
|
|
<div class="tt-file-gallery-overlay" @click.stop="openViewer(file)"><i class="fas fa-search-plus"></i>
|
|
</div>
|
|
</div>
|
|
<div class="tt-file-gallery-filename" :title="file.fileName">
|
|
<span>{{ file.fileName }}</span>
|
|
<i v-if="editMode" class="fas fa-edit text-primary ml-1 action-icon"
|
|
@click="startEdit(file, $event)"></i>
|
|
<i v-if="deleteMode" class="fas fa-trash text-danger ml-1 action-icon"
|
|
@click="deleteFile(file, $event)"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<tt-modal v-if="editingFile" :show="true" title="Dokument bearbeiten" @update:show="cancelEdit"
|
|
@submit="saveEdit" :delete="false" :disable-min-height="true">
|
|
<slot name="file-edit" :file="editingFile"></slot>
|
|
</tt-modal>
|
|
<tt-fullscreen-viewer v-if="fullscreenItem" :item="fullscreenItem" :items="imageFiles"
|
|
:initial-index="imageFiles.findIndex(f => f.id === fullscreenItem.id)"
|
|
@close="closeViewer"/>
|
|
</div>`
|
|
}); |