diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 5079934..b1ee49a 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/styles/pdf-builder.css b/src/main/frontend/styles/pdf-builder.css index 0e50ca1..808536b 100644 --- a/src/main/frontend/styles/pdf-builder.css +++ b/src/main/frontend/styles/pdf-builder.css @@ -80,14 +80,70 @@ .inspector-item { display: flex; - align-items: center; - gap: var(--lumo-space-s); - padding: var(--lumo-space-xs) 0; + flex-direction: column; + gap: var(--lumo-space-xs); + padding: var(--lumo-space-s) 0; 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 { width: 100%; diff --git a/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java b/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java index 0f6ecac..5dc3c27 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java @@ -38,10 +38,13 @@ import java.util.Base64; import java.util.Objects; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.NumberField; +import com.vaadin.flow.component.KeyPressEvent; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; @Route(value = "pdf-builder", layout = MainLayout.class) @PageTitle("PDF Builder") @@ -57,6 +60,10 @@ public class PDFBuilderView extends Div { // Serverseitige Fallback-Liste der Frames (zeigt Einträge selbst dann an, // wenn das DOM-Lesen via JS temporär fehlschlägt) private final List serverFrames = new ArrayList<>(); + // Track which items are collapsed by their ID + private final Set collapsedItems = new HashSet<>(); + // Temporarily store element name from dialog + private String currentElementName; public PDFBuilderView(PDFGenerationService pdfService, PdfTemplateService templateService) { this.pdfService = pdfService; @@ -141,13 +148,14 @@ public class PDFBuilderView extends Div { " 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 name = f.getAttribute('data-name') || '';\n" + " const img = f.querySelector('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" + " const editor = f.querySelector('.text-editor');\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" + " return JSON.stringify({canvas:{width: Math.round(rect.width), height: Math.round(rect.height)}, items});\n" + @@ -160,17 +168,87 @@ public class PDFBuilderView extends Div { private Div createInspector() { Div inspector = new Div(); inspector.addClassName("pdf-inspector"); - H3 title = new H3("Elemente auf Canvas"); + + H3 title = new H3("Elemente"); 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); + refresh.getStyle().set("width", "100%"); + refresh.getStyle().set("margin-top", "var(--lumo-space-s)"); + + inspector.add(title, inspectorList, refresh); + 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() { String js = getFramesInspectorJs(); @@ -217,19 +295,55 @@ public class PDFBuilderView extends Div { if (f == null) { continue; } Div row = new Div(); row.addClassName("inspector-item"); - String idText = (f.id == null ? "" : f.id); - String typeText = ("text".equals(f.type) ? "Text" : "Bild"); - Span label = new Span(typeText + " (" + idText + ")"); + + // Set unique ID for this inspector item to track collapse state + 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 y = makeField("Y", f.y); NumberField w = makeField("Breite", f.width); 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()); }); 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); + + // Add fields to container vertically + fieldsContainer.add(x, y, w, h); + + // Add header and fields to row + row.add(itemHeader, fieldsContainer); 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'; })()"; UI.getCurrent().getPage().executeJs(js); // 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) 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; FrameInfo found = null; 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; serverFrames.add(found); } + if (name != null) found.name = name; if (type != null) found.type = type; if (x != null) found.x = x; if (y != null) found.y = y; @@ -286,12 +401,13 @@ public class PDFBuilderView extends Div { " const items = frames.map(f=>{\n" + " const cs = getComputedStyle(f);\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 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" + + " return {id, name, type, x, y, width, height};\n" + " });\n" + " return JSON.stringify(items);\n" + "})()"; @@ -299,6 +415,7 @@ public class PDFBuilderView extends Div { private static class FrameInfo { public String id; + public String name; public String type; public Integer x; public Integer y; @@ -466,9 +583,9 @@ public class PDFBuilderView extends Div { @com.vaadin.flow.component.ClientCallable private void onDrop(String tool, int x, int y) { if (Objects.equals(tool, "text")) { - addTextFrame(x, y); + showElementNameDialog("Text", () -> addTextFrame(x, y)); } 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.getElement().setAttribute("data-frame-id", id); frame.getElement().setAttribute("data-type", "text"); + frame.getElement().setAttribute("data-name", currentElementName != null ? currentElementName : "Text"); frame.getStyle().set("left", x + "px"); frame.getStyle().set("top", y + "px"); frame.getStyle().set("width", "300px"); @@ -547,7 +665,7 @@ public class PDFBuilderView extends Div { // Aktivierungsverhalten enableFrameActivation(frame); // 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); // Zusätzlich clientseitig deferred refresher, um DOM-Werte zu synchronisieren PDFBuilderView.this.refreshInspectorFromDomDeferred(); @@ -587,6 +705,7 @@ public class PDFBuilderView extends Div { frame.setId(id); frame.getElement().setAttribute("data-frame-id", id); frame.getElement().setAttribute("data-type", "image"); + frame.getElement().setAttribute("data-name", currentElementName != null ? currentElementName : "Bild"); frame.getStyle().set("left", x + "px"); frame.getStyle().set("top", y + "px"); frame.getStyle().set("width", "260px"); @@ -611,7 +730,7 @@ public class PDFBuilderView extends Div { // Aktivierungsverhalten enableFrameActivation(frame); // Serverliste sofort updaten und rendern - upsertServerFrame(id, "image", x, y, 260, 180); + upsertServerFrame(id, currentElementName, "image", x, y, 260, 180); renderInspectorItems(serverFrames); // Zusätzlich deferred refresher PDFBuilderView.this.refreshInspectorFromDomDeferred();