Initial commit
This commit is contained in:
622
skrift-configurator/assets/js/configurator-preview-manager.js
Normal file
622
skrift-configurator/assets/js/configurator-preview-manager.js
Normal file
@@ -0,0 +1,622 @@
|
||||
/**
|
||||
* Preview Manager - Verwaltet Preview-Generation mit Batch-Loading, Navigation und Rate-Limiting
|
||||
*/
|
||||
|
||||
import { preparePlaceholdersForIndex } from './configurator-utils.js';
|
||||
|
||||
class PreviewManager {
|
||||
constructor(api) {
|
||||
this.api = api;
|
||||
this.currentBatchIndex = 0;
|
||||
this.currentDocIndex = 0;
|
||||
this.previewCount = 0;
|
||||
this.batchSize = 25;
|
||||
this.currentBatchPreviews = [];
|
||||
this.requestsRemaining = 10;
|
||||
this.maxRequests = 10;
|
||||
// Für Änderungserkennung und Validierungs-Caching
|
||||
this.lastValidatedTextHash = null;
|
||||
this.lastValidationResult = null;
|
||||
this.lastOverflowFiles = null; // null = keine Validierung durchgeführt
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt einen einfachen Hash für Text-Vergleich
|
||||
*/
|
||||
hashText(text, quantity, format, font) {
|
||||
const str = `${text || ''}|${quantity || 1}|${format || 'a4'}|${font || 'tilda'}`;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob sich der Text seit der letzten Validierung geändert hat
|
||||
*/
|
||||
hasTextChanged(state) {
|
||||
const currentHash = this.hashText(
|
||||
state.answers?.letterText,
|
||||
state.answers?.quantity,
|
||||
state.answers?.format,
|
||||
state.answers?.font
|
||||
);
|
||||
return currentHash !== this.lastValidatedTextHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert den aktuellen Text-Hash nach Validierung
|
||||
*/
|
||||
saveTextHash(state) {
|
||||
this.lastValidatedTextHash = this.hashText(
|
||||
state.answers?.letterText,
|
||||
state.answers?.quantity,
|
||||
state.answers?.format,
|
||||
state.answers?.font
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet Platzhalter für ein bestimmtes Dokument vor
|
||||
* Verwendet jetzt die gemeinsame Utility-Funktion
|
||||
*/
|
||||
preparePlaceholders(state, index) {
|
||||
return preparePlaceholdersForIndex(state, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert ein Letter-Objekt für ein bestimmtes Dokument
|
||||
*/
|
||||
prepareLetter(state, index, isEnvelope = false) {
|
||||
const placeholders = this.preparePlaceholders(state, index);
|
||||
|
||||
if (isEnvelope) {
|
||||
const isRecipientMode = state.answers?.envelopeMode === 'recipientData';
|
||||
const isCustomMode = state.answers?.envelopeMode === 'customText';
|
||||
const addressMode = state.addressMode || 'classic';
|
||||
|
||||
let envelopeText = '';
|
||||
let envelopeType = 'recipient';
|
||||
|
||||
if (isRecipientMode) {
|
||||
if (addressMode === 'free' && Array.isArray(state.freeAddressRows) && state.freeAddressRows.length > index) {
|
||||
// Freie Adresse: Bis zu 5 Zeilen
|
||||
const freeAddr = state.freeAddressRows[index];
|
||||
const lines = [];
|
||||
if (freeAddr.line1) lines.push(freeAddr.line1);
|
||||
if (freeAddr.line2) lines.push(freeAddr.line2);
|
||||
if (freeAddr.line3) lines.push(freeAddr.line3);
|
||||
if (freeAddr.line4) lines.push(freeAddr.line4);
|
||||
if (freeAddr.line5) lines.push(freeAddr.line5);
|
||||
envelopeText = lines.join('\n');
|
||||
envelopeType = 'free';
|
||||
} else if (addressMode === 'classic' && Array.isArray(state.recipientRows) && state.recipientRows.length > index) {
|
||||
// Klassische Adresse
|
||||
const recipient = state.recipientRows[index];
|
||||
const lines = [];
|
||||
|
||||
const fullName = `${recipient.firstName || ''} ${recipient.lastName || ''}`.trim();
|
||||
if (fullName) lines.push(fullName);
|
||||
|
||||
const streetLine = `${recipient.street || ''} ${recipient.houseNumber || ''}`.trim();
|
||||
if (streetLine) lines.push(streetLine);
|
||||
|
||||
const location = `${recipient.zip || ''} ${recipient.city || ''}`.trim();
|
||||
if (location) lines.push(location);
|
||||
|
||||
if (recipient.country && recipient.country !== 'Deutschland') {
|
||||
lines.push(recipient.country);
|
||||
}
|
||||
|
||||
envelopeText = lines.join('\n');
|
||||
envelopeType = 'recipient';
|
||||
}
|
||||
} else if (isCustomMode) {
|
||||
envelopeText = state.answers?.envelopeCustomText || '';
|
||||
envelopeType = 'custom';
|
||||
}
|
||||
|
||||
return {
|
||||
index: index,
|
||||
text: envelopeText,
|
||||
font: state.answers?.envelopeFont || 'tilda',
|
||||
format: state.answers?.format === 'a4' ? 'DIN_LANG' : 'C6',
|
||||
placeholders: placeholders,
|
||||
type: 'envelope',
|
||||
envelopeType: envelopeType,
|
||||
envelope: {
|
||||
type: envelopeType
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
index: index,
|
||||
text: state.answers?.letterText || state.answers?.text || state.answers?.briefText || '',
|
||||
font: state.answers?.font || 'tilda',
|
||||
format: state.answers?.format || 'a4',
|
||||
placeholders: placeholders,
|
||||
type: 'letter'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt ALLE Previews auf einmal
|
||||
* @param {boolean} skipLimitCheck - Wenn true, wird das Request-Limit ignoriert (für Validierung)
|
||||
*/
|
||||
async loadAllPreviews(state, isEnvelope = false, skipLimitCheck = false) {
|
||||
// Request-Limit nur prüfen wenn nicht übersprungen (normale Preview-Generierung)
|
||||
if (!skipLimitCheck) {
|
||||
if (this.requestsRemaining <= 0) {
|
||||
throw new Error(`Maximale Anzahl von ${this.maxRequests} Vorschau-Anfragen erreicht.`);
|
||||
}
|
||||
this.requestsRemaining--;
|
||||
}
|
||||
|
||||
const letters = [];
|
||||
for (let i = 0; i < this.previewCount; i++) {
|
||||
letters.push(this.prepareLetter(state, i, isEnvelope));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.api.generatePreviewBatch(letters);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Preview generation failed');
|
||||
}
|
||||
|
||||
this.currentBatchPreviews = result.previews;
|
||||
this.lastValidationResult = result; // Speichere für Overflow-Check
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Load error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Textlänge durch Preview-Generierung
|
||||
* Wenn Text nicht geändert wurde und Preview bereits existiert, wird gecachtes Ergebnis verwendet
|
||||
* @returns {Object} { valid: boolean, overflowFiles: Array, fromCache: boolean }
|
||||
*/
|
||||
async validateTextLength(state, forceRevalidate = false) {
|
||||
// Prüfe ob wir gecachtes Ergebnis verwenden können
|
||||
if (!forceRevalidate && !this.hasTextChanged(state) && this.lastOverflowFiles !== null) {
|
||||
console.log('[PreviewManager] Using cached validation result');
|
||||
return {
|
||||
valid: this.lastOverflowFiles.length === 0,
|
||||
overflowFiles: this.lastOverflowFiles,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
|
||||
this.previewCount = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
try {
|
||||
// skipLimitCheck = true: Validierung soll immer möglich sein, auch ohne verbleibende Anfragen
|
||||
await this.loadAllPreviews(state, false, true);
|
||||
|
||||
const result = this.lastValidationResult;
|
||||
if (!result) {
|
||||
this.lastOverflowFiles = [];
|
||||
this.saveTextHash(state);
|
||||
return { valid: true, overflowFiles: [], fromCache: false };
|
||||
}
|
||||
|
||||
const hasOverflow = result.hasOverflow || false;
|
||||
const overflowFiles = (result.overflowFiles || []).map(f => ({
|
||||
index: f.index,
|
||||
lineCount: f.lineCount,
|
||||
lineLimit: f.lineLimit
|
||||
}));
|
||||
|
||||
// Cache das Ergebnis
|
||||
this.lastOverflowFiles = overflowFiles;
|
||||
this.saveTextHash(state);
|
||||
|
||||
return {
|
||||
valid: !hasOverflow,
|
||||
overflowFiles: overflowFiles,
|
||||
fromCache: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Validation error:', error);
|
||||
|
||||
// Bei Fehlern: Nicht durchlassen - Nutzer muss es erneut versuchen
|
||||
return {
|
||||
valid: false,
|
||||
overflowFiles: [],
|
||||
error: error.message,
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt gecachte Overflow-Infos zurück (ohne neue Anfrage)
|
||||
*/
|
||||
getCachedOverflowFiles() {
|
||||
return this.lastOverflowFiles || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Validierung bereits erfolgt ist (für UI-Anzeige)
|
||||
*/
|
||||
hasValidationResult() {
|
||||
return this.lastOverflowFiles !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Previews und zeigt Overflow-Warnung wenn nötig
|
||||
* @returns {Object} { success: boolean, hasOverflow: boolean, overflowFiles: Array }
|
||||
*/
|
||||
async generatePreviews(state, dom, isEnvelope = false) {
|
||||
const btn = dom.querySelector('.sk-preview-generate-btn');
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
const requestCounterEl = dom.querySelector('.sk-preview-request-counter');
|
||||
|
||||
if (!btn) return { success: false, hasOverflow: false, overflowFiles: [] };
|
||||
|
||||
try {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generiere Vorschau...';
|
||||
|
||||
this.previewCount = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Lade alle ${this.previewCount} Dokumente...`;
|
||||
}
|
||||
|
||||
await this.loadAllPreviews(state, isEnvelope);
|
||||
|
||||
// Text-Hash speichern für Änderungserkennung
|
||||
if (!isEnvelope) {
|
||||
this.saveTextHash(state);
|
||||
}
|
||||
|
||||
this.currentDocIndex = 0;
|
||||
this.showPreview(0, dom);
|
||||
this.showNavigationControls(dom, state, isEnvelope);
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Dokument 1 von ${this.previewCount}`;
|
||||
}
|
||||
|
||||
if (requestCounterEl) {
|
||||
requestCounterEl.textContent = `Verbleibende Vorschau-Anfragen: ${this.requestsRemaining}/${this.maxRequests}`;
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.textContent.includes('Umschlag') ? 'Umschlag Vorschau generieren' : 'Vorschau Schriftstück generieren';
|
||||
|
||||
// Overflow-Prüfung für Briefe (nicht Umschläge)
|
||||
if (!isEnvelope && this.lastValidationResult) {
|
||||
const hasOverflow = this.lastValidationResult.hasOverflow || false;
|
||||
const overflowFiles = (this.lastValidationResult.overflowFiles || []).map(f => ({
|
||||
index: f.index,
|
||||
lineCount: f.lineCount,
|
||||
lineLimit: f.lineLimit
|
||||
}));
|
||||
|
||||
// Cache speichern
|
||||
this.lastOverflowFiles = overflowFiles;
|
||||
|
||||
return { success: true, hasOverflow, overflowFiles };
|
||||
}
|
||||
|
||||
return { success: true, hasOverflow: false, overflowFiles: [] };
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Error:', error);
|
||||
btn.disabled = false;
|
||||
btn.textContent = isEnvelope ? 'Umschlag Vorschau generieren' : 'Vorschau generieren';
|
||||
|
||||
// Fehlermeldung im Preview-Bereich anzeigen
|
||||
const previewBox = dom.querySelector('.sk-preview-box');
|
||||
if (previewBox) {
|
||||
previewBox.innerHTML = '';
|
||||
const notice = document.createElement('div');
|
||||
notice.style.cssText = 'padding: 20px; text-align: center; color: #666;';
|
||||
notice.innerHTML = `
|
||||
<p style="margin-bottom: 15px;">Die Vorschau konnte nicht generiert werden.</p>
|
||||
<p style="color: #999; font-size: 13px;">Bitte versuchen Sie es erneut oder kontaktieren Sie uns bei anhaltenden Problemen.</p>
|
||||
`;
|
||||
previewBox.appendChild(notice);
|
||||
}
|
||||
|
||||
return { success: false, hasOverflow: false, overflowFiles: [], error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Preview an einem bestimmten Index
|
||||
*/
|
||||
showPreview(index, dom) {
|
||||
const preview = this.currentBatchPreviews[index];
|
||||
if (!preview) {
|
||||
console.warn('[PreviewManager] Preview not loaded:', index);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewBox = dom.querySelector('.sk-preview-box');
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
|
||||
if (!previewBox) {
|
||||
console.warn('[PreviewManager] Preview box not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.style.cssText = 'position: relative; overflow: hidden; margin-top: 15px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = `${this.api.baseURL}${preview.url}?t=${Date.now()}`;
|
||||
img.style.cssText = 'width: 100%; display: block;';
|
||||
|
||||
imgContainer.addEventListener('click', () => {
|
||||
this.showFullscreenPreview(img.src);
|
||||
});
|
||||
|
||||
imgContainer.addEventListener('mouseenter', () => {
|
||||
imgContainer.style.opacity = '0.9';
|
||||
});
|
||||
|
||||
imgContainer.addEventListener('mouseleave', () => {
|
||||
imgContainer.style.opacity = '1';
|
||||
});
|
||||
|
||||
imgContainer.appendChild(img);
|
||||
previewBox.innerHTML = '';
|
||||
previewBox.appendChild(imgContainer);
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Dokument ${index + 1} von ${this.previewCount}`;
|
||||
}
|
||||
|
||||
this.currentDocIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigiert zur nächsten/vorherigen Preview
|
||||
*/
|
||||
navigateWithinBatch(direction, dom) {
|
||||
const newIndex = this.currentDocIndex + direction;
|
||||
|
||||
if (newIndex < 0 || newIndex >= this.currentBatchPreviews.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPreview(newIndex, dom);
|
||||
this.updateNavigationButtons();
|
||||
}
|
||||
|
||||
// navigateToBatch wurde entfernt - Batch-Loading wird nicht mehr verwendet
|
||||
// Alle Previews werden jetzt auf einmal geladen (loadAllPreviews)
|
||||
|
||||
/**
|
||||
* Ermittelt den aktuellen lokalen Index
|
||||
*/
|
||||
getCurrentLocalIndex(dom) {
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
if (!statusEl) return 0;
|
||||
|
||||
const match = statusEl.textContent.match(/Dokument (\d+) von/);
|
||||
if (match) {
|
||||
return parseInt(match[1]) - 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Request Counter im DOM
|
||||
*/
|
||||
updateRequestCounter(dom) {
|
||||
const requestCounterEl = dom.querySelector('.sk-preview-request-counter');
|
||||
if (requestCounterEl) {
|
||||
requestCounterEl.textContent = `Verbleibende Vorschau-Anfragen: ${this.requestsRemaining}/${this.maxRequests}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert Button-States der Navigation
|
||||
*/
|
||||
updateNavigationButtons() {
|
||||
const navWrapper = document.querySelector('.sk-preview-navigation-container');
|
||||
if (!navWrapper) return;
|
||||
|
||||
const navContainer = navWrapper.querySelector('.sk-preview-navigation');
|
||||
if (!navContainer) return;
|
||||
|
||||
const prevDocBtn = navContainer.querySelector('.sk-preview-prev-doc');
|
||||
const nextDocBtn = navContainer.querySelector('.sk-preview-next-doc');
|
||||
|
||||
if (prevDocBtn) {
|
||||
const canPrev = this.currentDocIndex > 0;
|
||||
prevDocBtn.disabled = !canPrev;
|
||||
prevDocBtn.style.opacity = canPrev ? '1' : '0.5';
|
||||
prevDocBtn.style.cursor = canPrev ? 'pointer' : 'not-allowed';
|
||||
}
|
||||
|
||||
if (nextDocBtn) {
|
||||
const canNext = this.currentDocIndex < this.currentBatchPreviews.length - 1;
|
||||
nextDocBtn.disabled = !canNext;
|
||||
nextDocBtn.style.opacity = canNext ? '1' : '0.5';
|
||||
nextDocBtn.style.cursor = canNext ? 'pointer' : 'not-allowed';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Navigation Controls
|
||||
*/
|
||||
showNavigationControls(dom, state, isEnvelope) {
|
||||
const navWrapper = dom.querySelector('.sk-preview-navigation-container');
|
||||
if (!navWrapper) return;
|
||||
|
||||
let navContainer = navWrapper.querySelector('.sk-preview-navigation');
|
||||
if (navContainer) {
|
||||
navContainer.remove();
|
||||
}
|
||||
|
||||
navContainer = document.createElement('div');
|
||||
navContainer.className = 'sk-preview-navigation';
|
||||
navContainer.style.display = 'flex';
|
||||
navContainer.style.gap = '10px';
|
||||
navContainer.style.alignItems = 'center';
|
||||
|
||||
const buttonStyle = {
|
||||
padding: '8px 12px',
|
||||
fontSize: '20px',
|
||||
lineHeight: '1',
|
||||
minWidth: '40px',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
};
|
||||
|
||||
const prevDocBtn = document.createElement('button');
|
||||
prevDocBtn.type = 'button';
|
||||
prevDocBtn.className = 'sk-btn sk-btn-secondary sk-preview-prev-doc';
|
||||
prevDocBtn.textContent = '‹';
|
||||
prevDocBtn.title = 'Vorheriges Dokument';
|
||||
Object.assign(prevDocBtn.style, buttonStyle);
|
||||
prevDocBtn.onclick = () => {
|
||||
this.navigateWithinBatch(-1, dom);
|
||||
};
|
||||
|
||||
const nextDocBtn = document.createElement('button');
|
||||
nextDocBtn.type = 'button';
|
||||
nextDocBtn.className = 'sk-btn sk-btn-secondary sk-preview-next-doc';
|
||||
nextDocBtn.textContent = '›';
|
||||
nextDocBtn.title = 'Nächstes Dokument';
|
||||
Object.assign(nextDocBtn.style, buttonStyle);
|
||||
nextDocBtn.onclick = () => {
|
||||
this.navigateWithinBatch(1, dom);
|
||||
};
|
||||
|
||||
navContainer.appendChild(prevDocBtn);
|
||||
navContainer.appendChild(nextDocBtn);
|
||||
|
||||
navWrapper.appendChild(navContainer);
|
||||
this.updateNavigationButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard Event Handler
|
||||
*/
|
||||
handleKeyboardNavigation(event, state, dom, isEnvelope = false) {
|
||||
if (this.currentBatchPreviews.length === 0) return;
|
||||
|
||||
const currentLocalIndex = this.getCurrentLocalIndex(dom);
|
||||
|
||||
if (event.key === 'ArrowLeft' && currentLocalIndex > 0) {
|
||||
event.preventDefault();
|
||||
this.navigateWithinBatch(-1, dom);
|
||||
} else if (event.key === 'ArrowRight' && currentLocalIndex < this.currentBatchPreviews.length - 1) {
|
||||
event.preventDefault();
|
||||
this.navigateWithinBatch(1, dom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
this.currentBatchPreviews = [];
|
||||
this.currentBatchIndex = 0;
|
||||
this.previewCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset für neue Session
|
||||
*/
|
||||
reset() {
|
||||
this.currentBatchPreviews = [];
|
||||
this.currentBatchIndex = 0;
|
||||
this.previewCount = 0;
|
||||
this.requestsRemaining = this.maxRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Fullscreen-Vorschau als Modal
|
||||
*/
|
||||
showFullscreenPreview(imgSrc) {
|
||||
const existingModal = document.getElementById('sk-preview-fullscreen-modal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'sk-preview-fullscreen-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const modalImg = document.createElement('img');
|
||||
modalImg.src = imgSrc;
|
||||
modalImg.style.cssText = `
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`;
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
font-size: 40px;
|
||||
color: white;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.textContent = 'Klicken zum Schließen';
|
||||
hint.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
modal.appendChild(modalImg);
|
||||
modal.appendChild(closeBtn);
|
||||
modal.appendChild(hint);
|
||||
|
||||
const closeModal = () => modal.remove();
|
||||
modal.addEventListener('click', closeModal);
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
}, { once: true });
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanzen
|
||||
window.envelopePreviewManager = null;
|
||||
window.contentPreviewManager = null;
|
||||
|
||||
export default PreviewManager;
|
||||
Reference in New Issue
Block a user