@@ -0,0 +1,520 @@
package de.assecutor.votianlt.pages.view ;
import com.vaadin.flow.component.UI ;
import com.vaadin.flow.component.button.Button ;
import com.vaadin.flow.component.button.ButtonVariant ;
import com.vaadin.flow.component.dependency.CssImport ;
import com.vaadin.flow.component.dialog.Dialog ;
import com.vaadin.flow.component.html.Anchor ;
import com.vaadin.flow.component.html.Div ;
import com.vaadin.flow.component.html.H3 ;
import com.vaadin.flow.component.html.Image ;
import com.vaadin.flow.component.html.Span ;
import com.vaadin.flow.component.icon.Icon ;
import com.vaadin.flow.component.icon.VaadinIcon ;
import com.vaadin.flow.component.notification.Notification ;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout ;
import com.vaadin.flow.component.orderedlayout.VerticalLayout ;
import com.vaadin.flow.component.page.PendingJavaScriptResult ;
import com.vaadin.flow.component.upload.Upload ;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer ;
import com.vaadin.flow.router.PageTitle ;
import com.vaadin.flow.router.Route ;
import com.vaadin.flow.router.Menu ;
import com.vaadin.flow.server.StreamResource ;
import com.vaadin.flow.server.StreamRegistration ;
import com.vaadin.flow.server.VaadinSession ;
import de.assecutor.votianlt.pages.base.ui.view.MainLayout ;
import de.assecutor.votianlt.pages.service.PDFGenerationService ;
import de.assecutor.votianlt.pages.service.PdfTemplateService ;
import jakarta.annotation.security.RolesAllowed ;
import java.io.ByteArrayInputStream ;
import java.io.IOException ;
import java.io.Serial ;
import java.nio.charset.StandardCharsets ;
import java.util.Base64 ;
import java.util.Objects ;
import com.vaadin.flow.component.textfield.TextField ;
@Route ( value = " pdf-builder " , layout = MainLayout . class )
@PageTitle ( " PDF Builder " )
@Menu ( order = 6 , icon = " vaadin:file-text " , title = " PDF Builder " )
@RolesAllowed ( " USER " )
@CssImport ( " ./styles/pdf-builder.css " )
public class PDFBuilderView extends Div {
private final PDFGenerationService pdfService ;
private final PdfTemplateService templateService ;
private final PdfCanvas canvas ;
public PDFBuilderView ( PDFGenerationService pdfService , PdfTemplateService templateService ) {
this . pdfService = pdfService ;
this . templateService = templateService ;
addClassName ( " pdf-builder-root " ) ;
setSizeFull ( ) ;
// Workspace (Palette links, Canvas mittig)
Div workspace = new Div ( ) ;
workspace . addClassName ( " pdf-builder-workspace " ) ;
Div palette = createPalette ( ) ;
canvas = new PdfCanvas ( ) ;
workspace . add ( palette , canvas ) ;
add ( workspace ) ;
// Aktionen unten: Speichern
HorizontalLayout actions = new HorizontalLayout ( ) ;
actions . addClassName ( " pdf-builder-actions " ) ;
actions . setWidthFull ( ) ;
Button save = new Button ( " Speichern " , e - > openSaveDialog ( ) ) ;
save . addThemeVariants ( ButtonVariant . LUMO_PRIMARY ) ;
actions . add ( save ) ;
add ( actions ) ;
}
private void openSaveDialog ( ) {
Dialog dlg = new Dialog ( ) ;
dlg . setHeaderTitle ( " Template speichern " ) ;
TextField name = new TextField ( " Template-Name " ) ;
name . setWidthFull ( ) ;
name . setRequired ( true ) ;
VerticalLayout content = new VerticalLayout ( name ) ;
content . setPadding ( false ) ;
content . setSpacing ( true ) ;
dlg . add ( content ) ;
Button cancel = new Button ( " Abbrechen " , e - > dlg . close ( ) ) ;
Button save = new Button ( " Speichern & PDF " , e - > {
if ( name . isEmpty ( ) ) {
Notification . show ( " Bitte einen Namen angeben " , 3000 , Notification . Position . MIDDLE ) ;
return ;
}
collectAndSave ( name . getValue ( ) ) ;
dlg . close ( ) ;
} ) ;
save . addThemeVariants ( ButtonVariant . LUMO_PRIMARY ) ;
dlg . getFooter ( ) . add ( cancel , save ) ;
dlg . open ( ) ;
}
private void collectAndSave ( String templateName ) {
String js = getCollectStateJs ( ) ;
PendingJavaScriptResult res = UI . getCurrent ( ) . getPage ( ) . executeJs ( js ) ;
res . then ( String . class , json - > {
try {
templateService . saveOrUpdate ( templateName , json ) ;
Notification . show ( " Template gespeichert " , 2000 , Notification . Position . BOTTOM_END ) ;
} catch ( Exception ex ) {
Notification . show ( " Speichern fehlgeschlagen: " + ex . getMessage ( ) , 4000 , Notification . Position . MIDDLE ) ;
}
// Danach PDF exportieren (Dateiname aus Template-Namen)
exportCanvasToPdf ( templateName ) ;
} ) ;
}
private String getCollectStateJs ( ) {
return " (function(){ \ n " +
" const root = document.getElementById('pdf-canvas'); \ n " +
" if(!root){ return JSON.stringify({items:[]}); } \ n " +
" root.classList.add('capturing'); \ n " +
" try { \ n " +
" const rootRect = root.getBoundingClientRect(); \ n " +
" const items = Array.from(root.querySelectorAll('.canvas-frame')).map(f=>{ \ n " +
" const r = f.getBoundingClientRect(); \ n " +
" const x = Math.round(r.left - rootRect.left); \ n " +
" const y = Math.round(r.top - rootRect.top); \ n " +
" const width = Math.round(r.width); \ n " +
" const height = Math.round(r.height); \ n " +
" const img = f.querySelector('img'); \ n " +
" if(img){ \ n " +
" return {type:'image', x, y, width, height, dataUrl: img.src}; \ n " +
" } else { \ n " +
" const editor = f.querySelector('.text-editor'); \ n " +
" const cs = editor ? getComputedStyle(editor) : null; \ n " +
" return {type:'text', x, y, width, height, html: editor?editor.innerHTML:'', style: cs?{fontSize: cs.fontSize, color: cs.color}:{}}; \ n " +
" } \ n " +
" }); \ n " +
" return JSON.stringify({items}); \ n " +
" } finally { \ n " +
" root.classList.remove('capturing'); \ n " +
" } \ n " +
" })() " ;
}
private Div createPalette ( ) {
Div palette = new Div ( ) ;
palette . addClassName ( " pdf-palette " ) ;
H3 title = new H3 ( " Elementpalette " ) ;
title . addClassName ( " palette-title " ) ;
palette . add ( title ) ;
// Text-Tool
Div textTool = new Div ( ) ;
textTool . addClassName ( " tool-item " ) ;
textTool . getElement ( ) . setProperty ( " draggable " , true ) ;
textTool . getElement ( ) . setAttribute ( " data-tool " , " text " ) ;
Icon textIcon = VaadinIcon . FONT . create ( ) ;
Span textLabel = new Span ( " Text hinzufügen " ) ;
textTool . add ( textIcon , textLabel ) ;
enableDragSource ( textTool ) ;
// Bild-Tool
Div imageTool = new Div ( ) ;
imageTool . addClassName ( " tool-item " ) ;
imageTool . getElement ( ) . setProperty ( " draggable " , true ) ;
imageTool . getElement ( ) . setAttribute ( " data-tool " , " image " ) ;
Icon imageIcon = VaadinIcon . PICTURE . create ( ) ;
Span imageLabel = new Span ( " Bild hinzufügen " ) ;
imageTool . add ( imageIcon , imageLabel ) ;
enableDragSource ( imageTool ) ;
palette . add ( textTool , imageTool ) ;
return palette ;
}
private void enableDragSource ( Div toolItem ) {
// HTML5-Drag-Daten setzen (Typ: text/image)
toolItem . getElement ( ) . executeJs (
" this.addEventListener('dragstart', (e) => { " +
" e.dataTransfer.setData('application/x-tool', this.dataset.tool); " +
" e.dataTransfer.effectAllowed = 'copy'; " +
" }); "
) ;
}
private void exportCanvasToPdf ( String templateName ) {
// html2canvas laden (falls nicht vorhanden) und INNERES Canvas erfassen
String js = " const el = document.getElementById('pdf-canvas'); \ n " +
" el.classList.add('capturing'); \ n " +
" const capture = () => html2canvas(el).then(c => c.toDataURL('image/png')); \ n " +
" if (window.html2canvas) { \ n " +
" return capture().then(r => { el.classList.remove('capturing'); return r; }).catch(err => { el.classList.remove('capturing'); return null; }); \ n " +
" } \ n " +
" return new Promise(resolve => { \ n " +
" const s = document.createElement('script'); \ n " +
" s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js'; \ n " +
" s.onload = () => capture().then(r => { el.classList.remove('capturing'); resolve(r); }).catch(() => { el.classList.remove('capturing'); resolve(null); }); \ n " +
" s.onerror = () => { el.classList.remove('capturing'); resolve(null); }; \ n " +
" document.head.appendChild(s); \ n " +
" }); " ;
PendingJavaScriptResult result = UI . getCurrent ( ) . getPage ( ) . executeJs ( js ) ;
result . then ( String . class , dataUrl - > {
try {
if ( dataUrl = = null | | ! dataUrl . startsWith ( " data:image/png;base64, " ) ) {
Notification . show ( " Fehler beim Erfassen des Canvas " , 3000 , Notification . Position . MIDDLE ) ;
return ;
}
String base64 = dataUrl . substring ( dataUrl . indexOf ( ',' ) + 1 ) ;
byte [ ] pngBytes = Base64 . getDecoder ( ) . decode ( base64 . getBytes ( StandardCharsets . UTF_8 ) ) ;
byte [ ] pdfBytes = pdfService . generatePdfFromImage ( pngBytes ) ;
String fn = ( templateName = = null | | templateName . isBlank ( ) ? " canvas-export " : templateName . trim ( ) )
. replaceAll ( " [ \\ \\ /:*? \" <>|]+ " , " _ " ) ;
if ( ! fn . toLowerCase ( ) . endsWith ( " .pdf " ) ) {
fn = fn + " .pdf " ;
}
StreamResource resource = new StreamResource ( fn , ( ) - > new ByteArrayInputStream ( pdfBytes ) ) ;
resource . setContentType ( " application/pdf " ) ;
resource . setCacheTime ( 0 ) ;
StreamRegistration reg = VaadinSession . getCurrent ( ) . getResourceRegistry ( ) . registerResource ( resource ) ;
String url = reg . getResourceUri ( ) . toString ( ) ;
// Öffnet den Download direkt – i.d.R. zuverlässig und nicht durch Popup-Blocker verhindert
UI . getCurrent ( ) . getPage ( ) . open ( url ) ;
Notification . show ( " PDF wurde erstellt und heruntergeladen " , 3000 , Notification . Position . BOTTOM_END ) ;
} catch ( Exception ex ) {
Notification . show ( " Fehler bei PDF-Erzeugung: " + ex . getMessage ( ) , 5000 , Notification . Position . MIDDLE ) ;
}
} ) ;
}
// Canvas-Komponente mit Drop-Handling und Frame-Erzeugung
static class PdfCanvas extends Div {
@Serial
private static final long serialVersionUID = 1L ;
private final Div inner ;
PdfCanvas ( ) {
addClassName ( " pdf-canvas-wrapper " ) ;
// Inneres Canvas-Element (relative, A4-Seitenverhältnis)
inner = new Div ( ) ;
inner . addClassName ( " pdf-canvas " ) ;
inner . setId ( " pdf-canvas " ) ;
add ( inner ) ;
enableDrop ( inner ) ;
enableCanvasDeactivation ( inner ) ;
}
private void enableDrop ( Div target ) {
// Dragover/Drops auf dem Canvas erfassen und an Server melden
target . getElement ( ) . executeJs (
" const canvas = this; \ n " +
" canvas.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }); \ n " +
" canvas.addEventListener('drop', e => { \ n " +
" e.preventDefault(); \ n " +
" const rect = canvas.getBoundingClientRect(); \ n " +
" const x = Math.round(e.clientX - rect.left); \ n " +
" const y = Math.round(e.clientY - rect.top); \ n " +
" const tool = e.dataTransfer.getData('application/x-tool'); \ n " +
" if (canvas.parentElement && canvas.parentElement.$server) { \ n " +
" canvas.parentElement.$server.onDrop(tool, x, y); \ n " +
" } \ n " +
" }); "
) ;
}
private void enableCanvasDeactivation ( Div target ) {
target . getElement ( ) . executeJs (
" const canvas = this; \ n " +
" function deactivateAll(){ \ n " +
" canvas.querySelectorAll('.canvas-frame.active').forEach(f => f.classList.remove('active')); \ n " +
" canvas.querySelectorAll('.text-editor').forEach(ed => { if (ed.blur) ed.blur(); }); \ n " +
" } \ n " +
" canvas.addEventListener('mousedown', (e) => { \ n " +
" const el = e.target; \ n " +
" if (el === canvas || !el.closest('.canvas-frame')) { deactivateAll(); } \ n " +
" }); \ n " +
" canvas.addEventListener('touchstart', (e) => { \ n " +
" const el = e.target; \ n " +
" if (el === canvas || !el.closest('.canvas-frame')) { deactivateAll(); } \ n " +
" }, { passive: true }); "
) ;
}
@com.vaadin.flow.component.ClientCallable
private void onDrop ( String tool , int x , int y ) {
if ( Objects . equals ( tool , " text " ) ) {
addTextFrame ( x , y ) ;
} else if ( Objects . equals ( tool , " image " ) ) {
promptImageUpload ( x , y ) ;
}
}
private void addTextFrame ( int x , int y ) {
Div frame = new Div ( ) ;
frame . addClassName ( " canvas-frame " ) ;
frame . addClassName ( " text-frame " ) ;
frame . getStyle ( ) . set ( " left " , x + " px " ) ;
frame . getStyle ( ) . set ( " top " , y + " px " ) ;
frame . getStyle ( ) . set ( " width " , " 300px " ) ;
frame . getStyle ( ) . set ( " height " , " 140px " ) ;
// Move-Handle (oben links)
Div moveHandle = new Div ( ) ;
moveHandle . addClassName ( " frame-handle-move " ) ;
moveHandle . setText ( " ⠿ " ) ;
// Toolbar
HorizontalLayout toolbar = new HorizontalLayout ( ) ;
toolbar . addClassName ( " text-toolbar " ) ;
toolbar . setPadding ( false ) ;
toolbar . setSpacing ( true ) ;
Button bBold = new Button ( new Icon ( VaadinIcon . BOLD ) ) ;
bBold . addThemeVariants ( ButtonVariant . LUMO_TERTIARY_INLINE ) ;
Button bItalic = new Button ( new Icon ( VaadinIcon . ITALIC ) ) ;
bItalic . addThemeVariants ( ButtonVariant . LUMO_TERTIARY_INLINE ) ;
Button bUnderline = new Button ( new Icon ( VaadinIcon . UNDERLINE ) ) ;
bUnderline . addThemeVariants ( ButtonVariant . LUMO_TERTIARY_INLINE ) ;
Button bSmaller = new Button ( new Icon ( VaadinIcon . MINUS ) ) ;
bSmaller . addThemeVariants ( ButtonVariant . LUMO_TERTIARY_INLINE ) ;
Button bLarger = new Button ( new Icon ( VaadinIcon . PLUS ) ) ;
bLarger . addThemeVariants ( ButtonVariant . LUMO_TERTIARY_INLINE ) ;
Button cBlack = new Button ( ) ; cBlack . getElement ( ) . getThemeList ( ) . add ( " tertiary-inline " ) ; cBlack . getStyle ( ) . set ( " background " , " #000 " ) ; cBlack . getStyle ( ) . set ( " width " , " 16px " ) ; cBlack . getStyle ( ) . set ( " height " , " 16px " ) ;
Button cRed = new Button ( ) ; cRed . getElement ( ) . getThemeList ( ) . add ( " tertiary-inline " ) ; cRed . getStyle ( ) . set ( " background " , " #e11 " ) ; cRed . getStyle ( ) . set ( " width " , " 16px " ) ; cRed . getStyle ( ) . set ( " height " , " 16px " ) ;
Button cBlue = new Button ( ) ; cBlue . getElement ( ) . getThemeList ( ) . add ( " tertiary-inline " ) ; cBlue . getStyle ( ) . set ( " background " , " #16c " ) ; cBlue . getStyle ( ) . set ( " width " , " 16px " ) ; cBlue . getStyle ( ) . set ( " height " , " 16px " ) ;
Div editor = new Div ( ) ;
editor . addClassName ( " text-editor " ) ;
editor . getElement ( ) . setAttribute ( " contenteditable " , " true " ) ;
editor . getElement ( ) . setAttribute ( " tabindex " , " 0 " ) ;
editor . getStyle ( ) . set ( " padding " , " 8px " ) ;
editor . getStyle ( ) . set ( " font-size " , " 16px " ) ;
editor . getStyle ( ) . set ( " line-height " , " 1.3 " ) ;
editor . setText ( " Text hier eingeben… " ) ;
// Button-Aktionen (clientseitig auf Selektion)
bBold . addClickListener ( e - > editor . getElement ( ) . executeJs ( " document.execCommand('bold') " ) ) ;
bItalic . addClickListener ( e - > editor . getElement ( ) . executeJs ( " document.execCommand('italic') " ) ) ;
bUnderline . addClickListener ( e - > editor . getElement ( ) . executeJs ( " document.execCommand('underline') " ) ) ;
bSmaller . addClickListener ( e - > editor . getElement ( ) . executeJs (
" const s = parseInt(getComputedStyle(this).fontSize)||16; this.style.fontSize=(Math.max(8,s-2))+'px'; " ) ) ;
bLarger . addClickListener ( e - > editor . getElement ( ) . executeJs (
" const s = parseInt(getComputedStyle(this).fontSize)||16; this.style.fontSize=(s+2)+'px'; " ) ) ;
cBlack . addClickListener ( e - > editor . getElement ( ) . executeJs ( " document.execCommand('foreColor', false, '#000000') " ) ) ;
cRed . addClickListener ( e - > editor . getElement ( ) . executeJs ( " document.execCommand('foreColor', false, '#cc1111') " ) ) ;
cBlue . addClickListener ( e - > editor . getElement ( ) . executeJs ( " document.execCommand('foreColor', false, '#1166cc') " ) ) ;
toolbar . add ( bBold , bItalic , bUnderline , bSmaller , bLarger , cBlack , cRed , cBlue ) ;
frame . add ( moveHandle , toolbar , editor ) ;
// Frame dem inneren Canvas hinzufügen
inner . add ( frame ) ;
// Drag-to-move aktivieren
enableFrameDragging ( frame ) ;
// Aktivierungsverhalten
enableFrameActivation ( frame ) ;
}
private void promptImageUpload ( int x , int y ) {
Dialog dlg = new Dialog ( ) ;
dlg . setHeaderTitle ( " Bild hochladen " ) ;
dlg . setModal ( true ) ;
MemoryBuffer buffer = new MemoryBuffer ( ) ;
Upload upload = new Upload ( buffer ) ;
upload . setAcceptedFileTypes ( " image/* " ) ;
upload . setMaxFiles ( 1 ) ;
upload . addSucceededListener ( succeededEvent - > {
try {
byte [ ] bytes = buffer . getInputStream ( ) . readAllBytes ( ) ;
addImageFrame ( x , y , bytes , succeededEvent . getFileName ( ) ) ;
dlg . close ( ) ;
} catch ( IOException ex ) {
Notification . show ( " Upload fehlgeschlagen: " + ex . getMessage ( ) , 5000 , Notification . Position . MIDDLE ) ;
}
} ) ;
Button cancel = new Button ( " Abbrechen " , e - > dlg . close ( ) ) ;
VerticalLayout content = new VerticalLayout ( new Span ( " Bitte eine Bilddatei auswählen. " ) , upload ) ;
content . setPadding ( false ) ;
content . setSpacing ( true ) ;
dlg . add ( content ) ;
dlg . getFooter ( ) . add ( cancel ) ;
dlg . open ( ) ;
}
private void addImageFrame ( int x , int y , byte [ ] imageBytes , String filename ) {
Div frame = new Div ( ) ;
frame . addClassName ( " canvas-frame " ) ;
frame . addClassName ( " image-frame " ) ;
frame . getStyle ( ) . set ( " left " , x + " px " ) ;
frame . getStyle ( ) . set ( " top " , y + " px " ) ;
frame . getStyle ( ) . set ( " width " , " 260px " ) ;
frame . getStyle ( ) . set ( " height " , " 180px " ) ;
String base64 = Base64 . getEncoder ( ) . encodeToString ( imageBytes ) ;
Image img = new Image ( " data:image/png;base64, " + base64 , filename = = null ? " Bild " : filename ) ;
img . getStyle ( ) . set ( " width " , " 100% " ) ;
img . getStyle ( ) . set ( " height " , " 100% " ) ;
img . getStyle ( ) . set ( " object-fit " , " contain " ) ;
// Move-Handle (oben links)
Div moveHandle = new Div ( ) ;
moveHandle . addClassName ( " frame-handle-move " ) ;
moveHandle . setText ( " ⠿ " ) ;
// Nur Bild (Toolbar für Bilder aktuell nicht notwendig)
frame . add ( moveHandle , img ) ;
inner . add ( frame ) ;
// Drag-to-move aktivieren
enableFrameDragging ( frame ) ;
// Aktivierungsverhalten
enableFrameActivation ( frame ) ;
}
private void enableFrameDragging ( Div frame ) {
frame . getElement ( ) . executeJs (
" const frame = this; \ n " +
" const root = document.getElementById('pdf-canvas'); \ n " +
" let startX = 0, startY = 0, startLeft = 0, startTop = 0; \ n " +
" frame.__dragMoved = false; \ n " +
" frame.__downOutsideEditor = false; \ n " +
" function pxToInt(v){ const n = parseInt(v); return isNaN(n)?0:n; } \ n " +
" function placeCaretAtEnd(el){ try{ const range=document.createRange(); range.selectNodeContents(el); range.collapse(false); const sel=window.getSelection(); sel.removeAllRanges(); sel.addRange(range);}catch(err){} } \ n " +
" function onDown(e){ \ n " +
" const target = e.target; \ n " +
" const isHandle = target && target.closest && target.closest('.frame-handle-move'); \ n " +
" // Toolbar-Klicks nie zum Ziehen verwenden \ n " +
" if (target && target.closest && target.closest('.text-toolbar')) { return; } \ n " +
" // Klick im Editor soll immer fokussieren und nicht ziehen \ n " +
" const editor = frame.querySelector('.text-editor'); \ n " +
" const clickedInEditor = target && target.closest && target.closest('.text-editor'); \ n " +
" if (!isHandle && clickedInEditor) { return; } \ n " +
" const p = (e.touches && e.touches[0]) ? e.touches[0] : e; \ n " +
" // Nicht ziehen, wenn in der Nähe der Resize-Kanten geklickt wurde (damit Resize funktioniert) \ n " +
" const rect = frame.getBoundingClientRect(); \ n " +
" const offsetX = p.clientX - rect.left; \ n " +
" const offsetY = p.clientY - rect.top; \ n " +
" const edge = 14; \ n " +
" if (!isHandle && (offsetX < edge || offsetY < edge || (rect.width - offsetX) < edge || (rect.height - offsetY) < edge)) { return; } \ n " +
" frame.__downOutsideEditor = !isHandle; \ n " +
" e.preventDefault(); \ n " +
" startX = p.clientX; startY = p.clientY; \ n " +
" startLeft = pxToInt(frame.style.left); \ n " +
" startTop = pxToInt(frame.style.top); \ n " +
" frame.__dragMoved = false; \ n " +
" frame.style.zIndex = String((parseInt(frame.style.zIndex)||0) + 1); \ n " +
" document.addEventListener('mousemove', onMove); \ n " +
" document.addEventListener('mouseup', onUp, { once: true }); \ n " +
" document.addEventListener('touchmove', onMove, { passive: false }); \ n " +
" document.addEventListener('touchend', onUp, { once: true }); \ n " +
" } \ n " +
" function onMove(e){ \ n " +
" const p = (e.touches && e.touches[0]) ? e.touches[0] : e; \ n " +
" if (!p) return; \ n " +
" e.preventDefault(); \ n " +
" const dx = p.clientX - startX; \ n " +
" const dy = p.clientY - startY; \ n " +
" if (Math.abs(dx) > 2 || Math.abs(dy) > 2) { frame.__dragMoved = true; } \ n " +
" const rootRect = root.getBoundingClientRect(); \ n " +
" const maxLeft = rootRect.width - frame.offsetWidth; \ n " +
" const maxTop = rootRect.height - frame.offsetHeight; \ n " +
" let newLeft = Math.min(Math.max(0, startLeft + dx), maxLeft); \ n " +
" let newTop = Math.min(Math.max(0, startTop + dy), maxTop); \ n " +
" frame.style.left = Math.round(newLeft) + 'px'; \ n " +
" frame.style.top = Math.round(newTop) + 'px'; \ n " +
" } \ n " +
" function onUp(){ \ n " +
" document.removeEventListener('mousemove', onMove); \ n " +
" document.removeEventListener('touchmove', onMove); \ n " +
" // Plain Click auf Frame (nicht im Editor, kein Drag): Editor fokussieren \ n " +
" if (!frame.__dragMoved && frame.__downOutsideEditor){ const ed = frame.querySelector('.text-editor'); if (ed){ ed.focus(); placeCaretAtEnd(ed); } } \ n " +
" // dragMoved-Flag nach dem Click-Zyklus zurücksetzen \ n " +
" setTimeout(()=>{ frame.__dragMoved = false; frame.__downOutsideEditor = false; }, 0); \ n " +
" } \ n " +
" frame.addEventListener('mousedown', onDown); \ n " +
" frame.addEventListener('touchstart', onDown, { passive: false }); \ n "
) ;
}
private void enableFrameActivation ( Div frame ) {
frame . getElement ( ) . executeJs (
" const frame = this; \ n " +
" function setActive(){ \ n " +
" const root = document.getElementById('pdf-canvas'); \ n " +
" if (!root) return; \ n " +
" root.querySelectorAll('.canvas-frame.active').forEach(f => f.classList.remove('active')); \ n " +
" frame.classList.add('active'); \ n " +
" } \ n " +
" frame.addEventListener('mousedown', ()=>{ setActive(); }); \ n " +
" frame.addEventListener('touchstart', ()=>{ setActive(); }, { passive: true }); \ n " +
" const editor = frame.querySelector('.text-editor'); \ n " +
" if (editor){ \ n " +
" editor.addEventListener('focus', ()=> setActive()); \ n " +
" editor.addEventListener('mousedown', ()=> setActive()); \ n " +
" } \ n " +
" function placeCaretAtEnd(el){ try{ const range=document.createRange(); range.selectNodeContents(el); range.collapse(false); const sel=window.getSelection(); sel.removeAllRanges(); sel.addRange(range);}catch(err){} } \ n " +
" frame.addEventListener('click', (e)=>{ \ n " +
" const t = e.target; \ n " +
" if (t && t.closest && t.closest('.text-toolbar')) return; \ n " +
" const ed = frame.querySelector('.text-editor'); \ n " +
" if (!ed) return; \ n " +
" // Wenn nicht direkt im Editor geklickt und kein Drag stattfand: Editor fokussieren \ n " +
" if (!(t && t.closest && t.closest('.text-editor')) && !frame.__dragMoved){ \ n " +
" ed.focus(); \ n " +
" placeCaretAtEnd(ed); \ n " +
" } \ n " +
" }); "
) ;
}
}
}