Erweiterungen

This commit is contained in:
2025-08-31 09:23:20 +02:00
parent c8967c3675
commit e435676668
3 changed files with 198 additions and 23 deletions

Binary file not shown.

View File

@@ -80,14 +80,70 @@
.inspector-item { .inspector-item {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: var(--lumo-space-s); gap: var(--lumo-space-xs);
padding: var(--lumo-space-xs) 0; padding: var(--lumo-space-s) 0;
border-bottom: 1px dashed var(--lumo-contrast-10pct); border-bottom: 1px dashed var(--lumo-contrast-10pct);
margin-bottom: var(--lumo-space-s);
}
.inspector-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.inspector-item .item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--lumo-space-xs);
}
.inspector-item .item-label {
font-weight: 500;
font-size: var(--lumo-font-size-s);
color: var(--lumo-body-text-color);
margin: 0;
}
.inspector-item .field-collapse-button {
background: none;
border: none;
cursor: pointer;
padding: var(--lumo-space-xs);
border-radius: var(--lumo-border-radius-s);
color: var(--lumo-secondary-text-color);
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
}
.inspector-item .field-collapse-button:hover {
background: var(--lumo-contrast-10pct);
color: var(--lumo-body-text-color);
}
.inspector-item .item-fields {
display: flex;
flex-direction: column;
gap: var(--lumo-space-xs);
transition: opacity 0.3s ease, max-height 0.3s ease;
max-height: 1000px;
overflow: hidden;
}
.inspector-item.fields-collapsed .item-fields {
opacity: 0;
max-height: 0;
margin: 0;
padding: 0;
}
.pdf-inspector vaadin-number-field {
width: 100%;
} }
.inspector-item:last-child { border-bottom: none; }
.inspector-item > span { min-width: 30%; font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color); }
.pdf-inspector vaadin-number-field { width: 65%; }
.pdf-canvas { .pdf-canvas {
width: 100%; width: 100%;

View File

@@ -38,10 +38,13 @@ import java.util.Base64;
import java.util.Objects; import java.util.Objects;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.KeyPressEvent;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List; import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
@Route(value = "pdf-builder", layout = MainLayout.class) @Route(value = "pdf-builder", layout = MainLayout.class)
@PageTitle("PDF Builder") @PageTitle("PDF Builder")
@@ -57,6 +60,10 @@ public class PDFBuilderView extends Div {
// Serverseitige Fallback-Liste der Frames (zeigt Einträge selbst dann an, // Serverseitige Fallback-Liste der Frames (zeigt Einträge selbst dann an,
// wenn das DOM-Lesen via JS temporär fehlschlägt) // wenn das DOM-Lesen via JS temporär fehlschlägt)
private final List<FrameInfo> serverFrames = new ArrayList<>(); private final List<FrameInfo> serverFrames = new ArrayList<>();
// Track which items are collapsed by their ID
private final Set<String> collapsedItems = new HashSet<>();
// Temporarily store element name from dialog
private String currentElementName;
public PDFBuilderView(PDFGenerationService pdfService, PdfTemplateService templateService) { public PDFBuilderView(PDFGenerationService pdfService, PdfTemplateService templateService) {
this.pdfService = pdfService; this.pdfService = pdfService;
@@ -141,13 +148,14 @@ public class PDFBuilderView extends Div {
" const width = Math.round(parseFloat(csf.width) || f.getBoundingClientRect().width);\n" + " const width = Math.round(parseFloat(csf.width) || f.getBoundingClientRect().width);\n" +
" const height = Math.round(parseFloat(csf.height) || f.getBoundingClientRect().height);\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 z = (function(){ const zc = parseInt(csf.zIndex); return isNaN(zc)? idx : zc; })();\n" +
" const name = f.getAttribute('data-name') || '';\n" +
" const img = f.querySelector('img');\n" + " const img = f.querySelector('img');\n" +
" if(img){\n" + " if(img){\n" +
" return {type:'image', x, y, width, height, z, dataUrl: img.src};\n" + " return {type:'image', name, x, y, width, height, z, dataUrl: img.src};\n" +
" } else {\n" + " } else {\n" +
" const editor = f.querySelector('.text-editor');\n" + " const editor = f.querySelector('.text-editor');\n" +
" const cs = editor ? getComputedStyle(editor) : null;\n" + " const cs = editor ? getComputedStyle(editor) : null;\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" + " return {type:'text', name, x, y, width, height, z, html: editor?editor.innerHTML:'', editorStyle: cs?{fontSize: cs.fontSize, color: cs.color, lineHeight: cs.lineHeight}:{}};\n" +
" }\n" + " }\n" +
" });\n" + " });\n" +
" return JSON.stringify({canvas:{width: Math.round(rect.width), height: Math.round(rect.height)}, items});\n" + " return JSON.stringify({canvas:{width: Math.round(rect.width), height: Math.round(rect.height)}, items});\n" +
@@ -160,18 +168,88 @@ public class PDFBuilderView extends Div {
private Div createInspector() { private Div createInspector() {
Div inspector = new Div(); Div inspector = new Div();
inspector.addClassName("pdf-inspector"); inspector.addClassName("pdf-inspector");
H3 title = new H3("Elemente auf Canvas");
H3 title = new H3("Elemente");
title.addClassName("inspector-title"); title.addClassName("inspector-title");
inspectorList = new VerticalLayout(); inspectorList = new VerticalLayout();
inspectorList.setPadding(false); inspectorList.setPadding(false);
inspectorList.setSpacing(false); inspectorList.setSpacing(false);
inspector.add(title, inspectorList);
Button refresh = new Button("Aktualisieren", e -> refreshInspectorFromDom()); Button refresh = new Button("Aktualisieren", e -> refreshInspectorFromDom());
refresh.addThemeVariants(ButtonVariant.LUMO_TERTIARY); refresh.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
inspector.add(refresh); refresh.getStyle().set("width", "100%");
refresh.getStyle().set("margin-top", "var(--lumo-space-s)");
inspector.add(title, inspectorList, refresh);
return inspector; return inspector;
} }
private void toggleItemFields(Div inspectorItem, Button fieldCollapseButton) {
// Get the item ID from the element's ID (remove prefix)
String fullId = inspectorItem.getId().orElse("");
String itemId = fullId.startsWith("inspector-item-") ? fullId.substring(15) : fullId;
boolean isCollapsed = collapsedItems.contains(itemId);
if (isCollapsed) {
// Expand fields
collapsedItems.remove(itemId);
inspectorItem.removeClassName("fields-collapsed");
fieldCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOWN));
} else {
// Collapse fields
collapsedItems.add(itemId);
inspectorItem.addClassName("fields-collapsed");
fieldCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_RIGHT));
}
}
private void showElementNameDialog(String elementType, Runnable onNameProvided) {
Dialog dlg = new Dialog();
dlg.setHeaderTitle("Element benennen");
dlg.setModal(true);
TextField nameField = new TextField("Element-Name");
nameField.setWidthFull();
nameField.setRequired(true);
nameField.setPlaceholder(elementType + " eingeben...");
nameField.focus();
VerticalLayout content = new VerticalLayout();
content.setPadding(false);
content.setSpacing(true);
content.add(new Span("Bitte geben Sie einen Namen für das " + elementType + "-Element ein:"), nameField);
dlg.add(content);
Button cancel = new Button("Abbrechen", e -> dlg.close());
Button confirm = new Button("Erstellen", e -> {
String name = nameField.getValue();
if (name == null || name.trim().isEmpty()) {
Notification.show("Bitte einen Namen eingeben", 3000, Notification.Position.MIDDLE);
nameField.focus();
return;
}
// Store the name temporarily for use in element creation
currentElementName = name.trim();
dlg.close();
onNameProvided.run();
});
confirm.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
// Enter key should trigger confirm
nameField.addKeyPressListener(e -> {
if (e.getKey().equals("Enter")) {
confirm.click();
}
});
dlg.getFooter().add(cancel, confirm);
dlg.open();
}
private void refreshInspectorFromDom() { private void refreshInspectorFromDom() {
String js = getFramesInspectorJs(); String js = getFramesInspectorJs();
UI.getCurrent().getPage().executeJs(js).then(String.class, json -> { UI.getCurrent().getPage().executeJs(js).then(String.class, json -> {
@@ -217,19 +295,55 @@ public class PDFBuilderView extends Div {
if (f == null) { continue; } if (f == null) { continue; }
Div row = new Div(); Div row = new Div();
row.addClassName("inspector-item"); row.addClassName("inspector-item");
String idText = (f.id == null ? "" : f.id);
String typeText = ("text".equals(f.type) ? "Text" : "Bild"); // Set unique ID for this inspector item to track collapse state
Span label = new Span(typeText + " (" + idText + ")"); String itemId = f.id != null ? f.id : "";
row.setId("inspector-item-" + itemId);
// Check if this item should be collapsed
boolean isItemCollapsed = collapsedItems.contains(itemId);
if (isItemCollapsed) {
row.addClassName("fields-collapsed");
}
// Create header for each tool with label and collapse button
Div itemHeader = new Div();
itemHeader.addClassName("item-header");
// Item label - use custom name if available, otherwise fallback to type
String displayName = (f.name != null && !f.name.trim().isEmpty()) ? f.name :
("text".equals(f.type) ? "Text" : "Bild");
Span label = new Span(displayName);
label.addClassName("item-label");
// Individual collapse button for this tool's fields
Icon buttonIcon = isItemCollapsed ? new Icon(VaadinIcon.ANGLE_RIGHT) : new Icon(VaadinIcon.ANGLE_DOWN);
Button fieldCollapseButton = new Button(buttonIcon);
fieldCollapseButton.addClassName("field-collapse-button");
fieldCollapseButton.addClickListener(e -> toggleItemFields(row, fieldCollapseButton));
itemHeader.add(label, fieldCollapseButton);
// Item fields container for vertical stacking
Div fieldsContainer = new Div();
fieldsContainer.addClassName("item-fields");
NumberField x = makeField("X", f.x); NumberField x = makeField("X", f.x);
NumberField y = makeField("Y", f.y); NumberField y = makeField("Y", f.y);
NumberField w = makeField("Breite", f.width); NumberField w = makeField("Breite", f.width);
NumberField h = makeField("Höhe", f.height); NumberField h = makeField("Höhe", f.height);
// Update handler (nur Client-Änderungen)
// Update handlers (nur Client-Änderungen)
x.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); }); 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()); }); 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()); }); 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()); }); h.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); });
row.add(label, x, y, w, h);
// Add fields to container vertically
fieldsContainer.add(x, y, w, h);
// Add header and fields to row
row.add(itemHeader, fieldsContainer);
inspectorList.add(row); inspectorList.add(row);
} }
} }
@@ -252,12 +366,12 @@ public class PDFBuilderView extends Div {
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'; })()"; 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); UI.getCurrent().getPage().executeJs(js);
// Serverliste aktualisieren // Serverliste aktualisieren
upsertServerFrame(id, null, (int)dx, (int)dy, (int)dw, (int)dh); upsertServerFrame(id, null, null, (int)dx, (int)dy, (int)dw, (int)dh);
// Danach Inspector neu laden (deferred, damit Styles/DOM sicher angewendet sind) // Danach Inspector neu laden (deferred, damit Styles/DOM sicher angewendet sind)
refreshInspectorFromDomDeferred(); refreshInspectorFromDomDeferred();
} }
private void upsertServerFrame(String id, String type, Integer x, Integer y, Integer width, Integer height) { private void upsertServerFrame(String id, String name, String type, Integer x, Integer y, Integer width, Integer height) {
if (id == null || id.isBlank()) return; if (id == null || id.isBlank()) return;
FrameInfo found = null; FrameInfo found = null;
for (FrameInfo fi : serverFrames) { if (fi != null && id.equals(fi.id)) { found = fi; break; } } for (FrameInfo fi : serverFrames) { if (fi != null && id.equals(fi.id)) { found = fi; break; } }
@@ -266,6 +380,7 @@ public class PDFBuilderView extends Div {
found.id = id; found.id = id;
serverFrames.add(found); serverFrames.add(found);
} }
if (name != null) found.name = name;
if (type != null) found.type = type; if (type != null) found.type = type;
if (x != null) found.x = x; if (x != null) found.x = x;
if (y != null) found.y = y; if (y != null) found.y = y;
@@ -286,12 +401,13 @@ public class PDFBuilderView extends Div {
" const items = frames.map(f=>{\n" + " const items = frames.map(f=>{\n" +
" const cs = getComputedStyle(f);\n" + " const cs = getComputedStyle(f);\n" +
" const id = f.id || f.getAttribute('data-frame-id') || '';\n" + " const id = f.id || f.getAttribute('data-frame-id') || '';\n" +
" const name = f.getAttribute('data-name') || '';\n" +
" const type = f.classList.contains('text-frame') ? 'text' : 'image';\n" + " const type = f.classList.contains('text-frame') ? 'text' : 'image';\n" +
" const x = Math.round(parseFloat(cs.left) || 0);\n" + " const x = Math.round(parseFloat(cs.left) || 0);\n" +
" const y = Math.round(parseFloat(cs.top) || 0);\n" + " const y = Math.round(parseFloat(cs.top) || 0);\n" +
" const width = Math.round(parseFloat(cs.width) || f.getBoundingClientRect().width);\n" + " const width = Math.round(parseFloat(cs.width) || f.getBoundingClientRect().width);\n" +
" const height = Math.round(parseFloat(cs.height) || f.getBoundingClientRect().height);\n" + " const height = Math.round(parseFloat(cs.height) || f.getBoundingClientRect().height);\n" +
" return {id, type, x, y, width, height};\n" + " return {id, name, type, x, y, width, height};\n" +
" });\n" + " });\n" +
" return JSON.stringify(items);\n" + " return JSON.stringify(items);\n" +
"})()"; "})()";
@@ -299,6 +415,7 @@ public class PDFBuilderView extends Div {
private static class FrameInfo { private static class FrameInfo {
public String id; public String id;
public String name;
public String type; public String type;
public Integer x; public Integer x;
public Integer y; public Integer y;
@@ -466,9 +583,9 @@ public class PDFBuilderView extends Div {
@com.vaadin.flow.component.ClientCallable @com.vaadin.flow.component.ClientCallable
private void onDrop(String tool, int x, int y) { private void onDrop(String tool, int x, int y) {
if (Objects.equals(tool, "text")) { if (Objects.equals(tool, "text")) {
addTextFrame(x, y); showElementNameDialog("Text", () -> addTextFrame(x, y));
} else if (Objects.equals(tool, "image")) { } else if (Objects.equals(tool, "image")) {
promptImageUpload(x, y); showElementNameDialog("Bild", () -> promptImageUpload(x, y));
} }
} }
@@ -486,6 +603,7 @@ public class PDFBuilderView extends Div {
frame.setId(id); frame.setId(id);
frame.getElement().setAttribute("data-frame-id", id); frame.getElement().setAttribute("data-frame-id", id);
frame.getElement().setAttribute("data-type", "text"); frame.getElement().setAttribute("data-type", "text");
frame.getElement().setAttribute("data-name", currentElementName != null ? currentElementName : "Text");
frame.getStyle().set("left", x + "px"); frame.getStyle().set("left", x + "px");
frame.getStyle().set("top", y + "px"); frame.getStyle().set("top", y + "px");
frame.getStyle().set("width", "300px"); frame.getStyle().set("width", "300px");
@@ -547,7 +665,7 @@ public class PDFBuilderView extends Div {
// Aktivierungsverhalten // Aktivierungsverhalten
enableFrameActivation(frame); enableFrameActivation(frame);
// Serverliste sofort updaten und rendern, damit der Eintrag direkt erscheint // Serverliste sofort updaten und rendern, damit der Eintrag direkt erscheint
upsertServerFrame(id, "text", x, y, 300, 140); upsertServerFrame(id, currentElementName, "text", x, y, 300, 140);
renderInspectorItems(serverFrames); renderInspectorItems(serverFrames);
// Zusätzlich clientseitig deferred refresher, um DOM-Werte zu synchronisieren // Zusätzlich clientseitig deferred refresher, um DOM-Werte zu synchronisieren
PDFBuilderView.this.refreshInspectorFromDomDeferred(); PDFBuilderView.this.refreshInspectorFromDomDeferred();
@@ -587,6 +705,7 @@ public class PDFBuilderView extends Div {
frame.setId(id); frame.setId(id);
frame.getElement().setAttribute("data-frame-id", id); frame.getElement().setAttribute("data-frame-id", id);
frame.getElement().setAttribute("data-type", "image"); frame.getElement().setAttribute("data-type", "image");
frame.getElement().setAttribute("data-name", currentElementName != null ? currentElementName : "Bild");
frame.getStyle().set("left", x + "px"); frame.getStyle().set("left", x + "px");
frame.getStyle().set("top", y + "px"); frame.getStyle().set("top", y + "px");
frame.getStyle().set("width", "260px"); frame.getStyle().set("width", "260px");
@@ -611,7 +730,7 @@ public class PDFBuilderView extends Div {
// Aktivierungsverhalten // Aktivierungsverhalten
enableFrameActivation(frame); enableFrameActivation(frame);
// Serverliste sofort updaten und rendern // Serverliste sofort updaten und rendern
upsertServerFrame(id, "image", x, y, 260, 180); upsertServerFrame(id, currentElementName, "image", x, y, 260, 180);
renderInspectorItems(serverFrames); renderInspectorItems(serverFrames);
// Zusätzlich deferred refresher // Zusätzlich deferred refresher
PDFBuilderView.this.refreshInspectorFromDomDeferred(); PDFBuilderView.this.refreshInspectorFromDomDeferred();