Erweiterungen
This commit is contained in:
Binary file not shown.
@@ -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%;
|
||||
|
||||
@@ -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<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) {
|
||||
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,18 +168,88 @@ 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();
|
||||
UI.getCurrent().getPage().executeJs(js).then(String.class, json -> {
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user