Erweiterungen
This commit is contained in:
Binary file not shown.
@@ -52,6 +52,35 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Inspector (right side) */
|
||||
.pdf-inspector {
|
||||
width: 300px;
|
||||
min-width: 260px;
|
||||
border: 1px solid var(--lumo-contrast-20pct);
|
||||
border-radius: var(--lumo-border-radius-m);
|
||||
background: var(--lumo-base-color);
|
||||
box-shadow: var(--lumo-box-shadow-xs);
|
||||
padding: var(--lumo-space-m);
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pdf-inspector .inspector-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--lumo-space-s);
|
||||
}
|
||||
|
||||
.inspector-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--lumo-space-s);
|
||||
padding: var(--lumo-space-xs) 0;
|
||||
border-bottom: 1px dashed var(--lumo-contrast-10pct);
|
||||
}
|
||||
.inspector-item:last-child { border-bottom: none; }
|
||||
.inspector-item > span { min-width: 90px; font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color); }
|
||||
.pdf-inspector vaadin-number-field { width: 120px; }
|
||||
|
||||
.pdf-canvas {
|
||||
width: 60%;
|
||||
aspect-ratio: 210 / 297; /* A4 */
|
||||
@@ -128,6 +157,12 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Export-only: make box backgrounds transparent */
|
||||
.pdf-canvas.capturing-export .canvas-frame,
|
||||
.pdf-canvas.capturing-export .text-editor {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Image frames: no scrollbars */
|
||||
.image-frame {
|
||||
overflow: hidden; /* hide scrollbars */
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -38,6 +37,10 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.List;
|
||||
|
||||
@Route(value = "pdf-builder", layout = MainLayout.class)
|
||||
@PageTitle("PDF Builder")
|
||||
@@ -48,7 +51,8 @@ public class PDFBuilderView extends Div {
|
||||
|
||||
private final PDFGenerationService pdfService;
|
||||
private final PdfTemplateService templateService;
|
||||
private final PdfCanvas canvas;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private VerticalLayout inspectorList;
|
||||
|
||||
public PDFBuilderView(PDFGenerationService pdfService, PdfTemplateService templateService) {
|
||||
this.pdfService = pdfService;
|
||||
@@ -61,9 +65,10 @@ public class PDFBuilderView extends Div {
|
||||
workspace.addClassName("pdf-builder-workspace");
|
||||
|
||||
Div palette = createPalette();
|
||||
canvas = new PdfCanvas();
|
||||
PdfCanvas canvas = new PdfCanvas();
|
||||
Div inspector = createInspector();
|
||||
|
||||
workspace.add(palette, canvas);
|
||||
workspace.add(palette, canvas, inspector);
|
||||
add(workspace);
|
||||
|
||||
// Aktionen unten: Speichern
|
||||
@@ -74,6 +79,9 @@ public class PDFBuilderView extends Div {
|
||||
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
actions.add(save);
|
||||
add(actions);
|
||||
|
||||
// initialer Refresh (leer zu Beginn)
|
||||
refreshInspectorFromDom();
|
||||
}
|
||||
|
||||
private void openSaveDialog() {
|
||||
@@ -116,34 +124,132 @@ public class PDFBuilderView extends Div {
|
||||
}
|
||||
|
||||
private String getCollectStateJs() {
|
||||
return "(function(){\n" +
|
||||
return "(function(){" +
|
||||
" 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 rect = root.getBoundingClientRect();\n" +
|
||||
" const items = Array.from(root.querySelectorAll('.canvas-frame')).map((f, idx)=>{\n" +
|
||||
" const csf = getComputedStyle(f);\n" +
|
||||
" const x = Math.round(parseFloat(csf.left) || 0);\n" +
|
||||
" const y = Math.round(parseFloat(csf.top) || 0);\n" +
|
||||
" const width = Math.round(parseFloat(csf.width) || f.getBoundingClientRect().width);\n" +
|
||||
" const height = Math.round(parseFloat(csf.height) || f.getBoundingClientRect().height);\n" +
|
||||
" const z = (function(){ const zc = parseInt(csf.zIndex); return isNaN(zc)? idx : zc; })();\n" +
|
||||
" const img = f.querySelector('img');\n" +
|
||||
" if(img){\n" +
|
||||
" return {type:'image', x, y, width, height, dataUrl: img.src};\n" +
|
||||
" return {type:'image', x, y, width, height, z, 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" +
|
||||
" return {type:'text', x, y, width, height, z, html: editor?editor.innerHTML:'', editorStyle: cs?{fontSize: cs.fontSize, color: cs.color, lineHeight: cs.lineHeight}:{}};\n" +
|
||||
" }\n" +
|
||||
" });\n" +
|
||||
" return JSON.stringify({items});\n" +
|
||||
" return JSON.stringify({canvas:{width: Math.round(rect.width), height: Math.round(rect.height)}, items});\n" +
|
||||
" } finally {\n" +
|
||||
" root.classList.remove('capturing');\n" +
|
||||
" }\n" +
|
||||
"})()";
|
||||
}
|
||||
|
||||
private Div createInspector() {
|
||||
Div inspector = new Div();
|
||||
inspector.addClassName("pdf-inspector");
|
||||
H3 title = new H3("Elemente auf Canvas");
|
||||
title.addClassName("inspector-title");
|
||||
inspectorList = new VerticalLayout();
|
||||
inspectorList.setPadding(false);
|
||||
inspectorList.setSpacing(false);
|
||||
inspector.add(title, inspectorList);
|
||||
Button refresh = new Button("Aktualisieren", e -> refreshInspectorFromDom());
|
||||
refresh.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
inspector.add(refresh);
|
||||
return inspector;
|
||||
}
|
||||
|
||||
private void refreshInspectorFromDom() {
|
||||
String js = getFramesInspectorJs();
|
||||
UI.getCurrent().getPage().executeJs(js).then(String.class, json -> {
|
||||
try {
|
||||
String safeJson = (json == null || json.isBlank()) ? "[]" : json;
|
||||
List<FrameInfo> frames = objectMapper.readValue(safeJson, new TypeReference<List<FrameInfo>>(){});
|
||||
renderInspectorItems(frames);
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Inspector-Update fehlgeschlagen: " + ex.getMessage(), 3000, Notification.Position.MIDDLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renderInspectorItems(List<FrameInfo> frames) {
|
||||
inspectorList.removeAll();
|
||||
if (frames == null || frames.isEmpty()) return;
|
||||
for (FrameInfo f : frames) {
|
||||
Div row = new Div();
|
||||
row.addClassName("inspector-item");
|
||||
Span label = new Span(("text".equals(f.type) ? "Text" : "Bild") + " (" + f.id + ")");
|
||||
NumberField x = makeField("X", f.x);
|
||||
NumberField y = makeField("Y", f.y);
|
||||
NumberField w = makeField("Breite", f.width);
|
||||
NumberField h = makeField("Höhe", f.height);
|
||||
// Update handler (nur Client-Änderungen)
|
||||
x.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); });
|
||||
y.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); });
|
||||
w.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); });
|
||||
h.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); });
|
||||
row.add(label, x, y, w, h);
|
||||
inspectorList.add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private NumberField makeField(String label, Integer value){
|
||||
NumberField nf = new NumberField(label);
|
||||
nf.setStep(1d);
|
||||
nf.setMin(0d);
|
||||
if (value != null) { nf.setValue(value.doubleValue()); }
|
||||
nf.setWidth("120px");
|
||||
return nf;
|
||||
}
|
||||
|
||||
private void setFrameRect(String id, Double x, Double y, Double w, Double h){
|
||||
if (id == null || id.isBlank()) return;
|
||||
double dx = x == null ? 0 : Math.max(0, x);
|
||||
double dy = y == null ? 0 : Math.max(0, y);
|
||||
double dw = w == null ? 10 : Math.max(10, w);
|
||||
double dh = h == null ? 10 : Math.max(10, h);
|
||||
String js = "(function(){ const el = document.getElementById('" + id + "'); if(!el) return; el.style.left='" + (int)dx + "px'; el.style.top='" + (int)dy + "px'; el.style.width='" + (int)dw + "px'; el.style.height='" + (int)dh + "px'; })()";
|
||||
UI.getCurrent().getPage().executeJs(js);
|
||||
// Danach Inspector neu laden
|
||||
refreshInspectorFromDom();
|
||||
}
|
||||
|
||||
private String getFramesInspectorJs(){
|
||||
return "(function(){\n" +
|
||||
" const root = document.getElementById('pdf-canvas');\n" +
|
||||
" if(!root){ return '[]'; }\n" +
|
||||
" const items = Array.from(root.querySelectorAll('.canvas-frame')).map(f=>{\n" +
|
||||
" const cs = getComputedStyle(f);\n" +
|
||||
" const id = f.id || f.getAttribute('data-frame-id') || '';\n" +
|
||||
" const type = f.classList.contains('text-frame') ? 'text' : 'image';\n" +
|
||||
" const x = Math.round(parseFloat(cs.left) || 0);\n" +
|
||||
" const y = Math.round(parseFloat(cs.top) || 0);\n" +
|
||||
" const width = Math.round(parseFloat(cs.width) || f.getBoundingClientRect().width);\n" +
|
||||
" const height = Math.round(parseFloat(cs.height) || f.getBoundingClientRect().height);\n" +
|
||||
" return {id, type, x, y, width, height};\n" +
|
||||
" });\n" +
|
||||
" return JSON.stringify(items);\n" +
|
||||
"})()";
|
||||
}
|
||||
|
||||
private static class FrameInfo {
|
||||
public String id;
|
||||
public String type;
|
||||
public Integer x;
|
||||
public Integer y;
|
||||
public Integer width;
|
||||
public Integer height;
|
||||
}
|
||||
|
||||
private Div createPalette() {
|
||||
Div palette = new Div();
|
||||
palette.addClassName("pdf-palette");
|
||||
@@ -189,16 +295,16 @@ public class PDFBuilderView extends Div {
|
||||
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" +
|
||||
"el.classList.add('capturing','capturing-export');\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" +
|
||||
" return capture().then(r => { el.classList.remove('capturing','capturing-export'); return r; }).catch(err => { el.classList.remove('capturing','capturing-export'); 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" +
|
||||
" s.onload = () => capture().then(r => { el.classList.remove('capturing','capturing-export'); resolve(r); }).catch(() => { el.classList.remove('capturing','capturing-export'); resolve(null); });\n" +
|
||||
" s.onerror = () => { el.classList.remove('capturing','capturing-export'); resolve(null); };\n" +
|
||||
" document.head.appendChild(s);\n" +
|
||||
"});";
|
||||
|
||||
@@ -236,11 +342,12 @@ public class PDFBuilderView extends Div {
|
||||
}
|
||||
|
||||
// Canvas-Komponente mit Drop-Handling und Frame-Erzeugung
|
||||
static class PdfCanvas extends Div {
|
||||
class PdfCanvas extends Div {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Div inner;
|
||||
private int frameCounter = 0;
|
||||
|
||||
PdfCanvas() {
|
||||
addClassName("pdf-canvas-wrapper");
|
||||
@@ -258,7 +365,7 @@ public class PDFBuilderView extends Div {
|
||||
private void enableDrop(Div target) {
|
||||
// Dragover/Drops auf dem Canvas erfassen und an Server melden
|
||||
target.getElement().executeJs(
|
||||
"const canvas = this;\n" +
|
||||
"const canvas = this;" +
|
||||
"canvas.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });\n" +
|
||||
"canvas.addEventListener('drop', e => {\n" +
|
||||
" e.preventDefault();\n" +
|
||||
@@ -300,10 +407,20 @@ public class PDFBuilderView extends Div {
|
||||
}
|
||||
}
|
||||
|
||||
@com.vaadin.flow.component.ClientCallable
|
||||
private void onFrameChanged() {
|
||||
// Nach Drag/Resize: Inspector-Seite aktualisieren
|
||||
PDFBuilderView.this.refreshInspectorFromDom();
|
||||
}
|
||||
|
||||
private void addTextFrame(int x, int y) {
|
||||
Div frame = new Div();
|
||||
frame.addClassName("canvas-frame");
|
||||
frame.addClassName("text-frame");
|
||||
String id = "frame-" + (++frameCounter);
|
||||
frame.setId(id);
|
||||
frame.getElement().setAttribute("data-frame-id", id);
|
||||
frame.getElement().setAttribute("data-type", "text");
|
||||
frame.getStyle().set("left", x + "px");
|
||||
frame.getStyle().set("top", y + "px");
|
||||
frame.getStyle().set("width", "300px");
|
||||
@@ -364,6 +481,8 @@ public class PDFBuilderView extends Div {
|
||||
enableFrameDragging(frame);
|
||||
// Aktivierungsverhalten
|
||||
enableFrameActivation(frame);
|
||||
// Inspector aktualisieren
|
||||
PDFBuilderView.this.refreshInspectorFromDom();
|
||||
}
|
||||
|
||||
private void promptImageUpload(int x, int y) {
|
||||
@@ -396,6 +515,10 @@ public class PDFBuilderView extends Div {
|
||||
Div frame = new Div();
|
||||
frame.addClassName("canvas-frame");
|
||||
frame.addClassName("image-frame");
|
||||
String id = "frame-" + (++frameCounter);
|
||||
frame.setId(id);
|
||||
frame.getElement().setAttribute("data-frame-id", id);
|
||||
frame.getElement().setAttribute("data-type", "image");
|
||||
frame.getStyle().set("left", x + "px");
|
||||
frame.getStyle().set("top", y + "px");
|
||||
frame.getStyle().set("width", "260px");
|
||||
@@ -419,12 +542,15 @@ public class PDFBuilderView extends Div {
|
||||
enableFrameDragging(frame);
|
||||
// Aktivierungsverhalten
|
||||
enableFrameActivation(frame);
|
||||
// Inspector aktualisieren
|
||||
PDFBuilderView.this.refreshInspectorFromDom();
|
||||
}
|
||||
|
||||
private void enableFrameDragging(Div frame) {
|
||||
frame.getElement().executeJs(
|
||||
"const frame = this;\n" +
|
||||
"const root = document.getElementById('pdf-canvas');\n" +
|
||||
"const canvas = root;\n" +
|
||||
"let startX = 0, startY = 0, startLeft = 0, startTop = 0;\n" +
|
||||
"frame.__dragMoved = false;\n" +
|
||||
"frame.__downOutsideEditor = false;\n" +
|
||||
@@ -478,11 +604,15 @@ public class PDFBuilderView extends Div {
|
||||
" 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" +
|
||||
" // Inspector informieren\n" +
|
||||
" try { if (canvas && canvas.parentElement && canvas.parentElement.$server && canvas.parentElement.$server.onFrameChanged) { canvas.parentElement.$server.onFrameChanged(); } } catch(e){}\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"
|
||||
"frame.addEventListener('touchstart', onDown, { passive: false });\n" +
|
||||
"// Größenänderungen beobachten (ResizeObserver)\n" +
|
||||
"if (window.ResizeObserver) { const ro = new ResizeObserver(()=>{ try { if (canvas && canvas.parentElement && canvas.parentElement.$server && canvas.parentElement.$server.onFrameChanged) { canvas.parentElement.$server.onFrameChanged(); } } catch(e){} }); ro.observe(frame); }\n"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user