Erweiterungen

This commit is contained in:
2025-08-30 16:33:42 +02:00
parent 846b3999f2
commit 8ec6cb55d4
8 changed files with 874 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
This directory is automatically generated by Vaadin and contains the pre-compiled
frontend files/resources for your project (frontend development bundle).
It should be added to Version Control System and committed, so that other developers
do not have to compile it again.
Frontend development bundle is automatically updated when needed:
- an npm/pnpm package is added with @NpmPackage or directly into package.json
- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript
- Vaadin add-on with front-end customizations is added
- Custom theme imports/assets added into 'theme.json' file
- Exported web component is added.
If your project development needs a hot deployment of the frontend changes,
you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions):
- set `vaadin.frontend.hotdeploy=true` in `application.properties`
- configure `vaadin-maven-plugin`:
```
<configuration>
<frontendHotdeploy>true</frontendHotdeploy>
</configuration>
```
- configure `jetty-maven-plugin`:
```
<configuration>
<systemProperties>
<vaadin.frontend.hotdeploy>true</vaadin.frontend.hotdeploy>
</systemProperties>
</configuration>
```
Read more [about Vaadin development mode](https://vaadin.com/docs/next/flow/configuration/development-mode#precompiled-bundle).

BIN
src/main/bundles/dev.bundle Normal file

Binary file not shown.

View File

@@ -0,0 +1,182 @@
/* Styles for PDF Builder View */
.pdf-builder-root {
display: flex;
flex-direction: column;
gap: var(--lumo-space-m);
width: 100%;
}
.pdf-builder-workspace {
display: flex;
gap: var(--lumo-space-l);
align-items: flex-start;
width: 100%;
}
.pdf-palette {
width: 260px;
min-width: 240px;
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);
}
.pdf-palette .palette-title {
font-weight: 600;
margin-bottom: var(--lumo-space-s);
}
.pdf-palette .tool-item {
display: flex;
align-items: center;
gap: var(--lumo-space-m);
padding: var(--lumo-space-s);
border: 1px dashed var(--lumo-contrast-20pct);
border-radius: var(--lumo-border-radius-s);
cursor: grab;
user-select: none;
}
.pdf-palette .tool-item + .tool-item { margin-top: var(--lumo-space-s); }
.pdf-palette .tool-item:hover {
background: var(--lumo-contrast-5pct);
}
.pdf-canvas-wrapper {
flex: 1 1 auto;
display: flex;
justify-content: center;
}
.pdf-canvas {
width: 60%;
aspect-ratio: 210 / 297; /* A4 */
position: relative;
background: white;
border: 1px solid var(--lumo-contrast-20pct);
box-shadow: var(--lumo-box-shadow-m);
background-image: linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px),
linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px);
background-size: 20px 20px;
}
.canvas-frame {
position: absolute;
border: 3px solid var(--lumo-contrast-30pct); /* inactive: light gray */
border-radius: var(--lumo-border-radius-s);
background: rgba(255,255,255,0.9);
resize: none;
overflow: auto;
min-width: 80px;
min-height: 60px;
cursor: default;
user-select: none;
}
.canvas-frame.active {
border: 3px solid var(--lumo-primary-color); /* active: blue */
box-shadow: none;
resize: both;
cursor: default;
}
.canvas-frame .text-editor {
width: 100%;
height: 100%;
cursor: text;
user-select: text;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
.canvas-frame .text-toolbar {
position: absolute;
left: 0;
top: -40px;
display: none;
align-items: center;
gap: var(--lumo-space-xs);
background: var(--lumo-base-color);
border: 1px solid var(--lumo-contrast-20pct);
border-radius: var(--lumo-border-radius-s);
box-shadow: var(--lumo-box-shadow-xs);
padding: 2px 4px;
cursor: default;
user-select: none;
}
.canvas-frame.active .text-toolbar { display: flex; }
.canvas-frame img { width: 100%; height: 100%; object-fit: contain; display: block; }
.pdf-builder-actions { display: flex; justify-content: flex-end; }
/* Capturing mode: hide frame borders, handles, toolbars for save/export */
.pdf-canvas.capturing .canvas-frame,
.pdf-canvas.capturing .canvas-frame.active {
border-color: transparent !important; /* hide border but keep layout */
box-shadow: none !important;
resize: none !important;
}
.pdf-canvas.capturing .frame-handle-move,
.pdf-canvas.capturing .text-toolbar {
display: none !important;
}
/* Image frames: no scrollbars */
.image-frame {
overflow: hidden; /* hide scrollbars */
-ms-overflow-style: none; /* IE/Edge */
scrollbar-width: none; /* Firefox */
}
.image-frame::-webkit-scrollbar { display: none; } /* WebKit */
/* Text frames: no scrolling allowed */
.text-frame {
overflow: hidden; /* disable scrolling */
-ms-overflow-style: none; /* IE/Edge */
scrollbar-width: none; /* Firefox */
}
.text-frame::-webkit-scrollbar { display: none; } /* WebKit */
/* Also disable scrolling inside the text editor */
.text-frame .text-editor {
overflow: hidden; /* disable scrolling */
-ms-overflow-style: none; /* IE/Edge */
scrollbar-width: none; /* Firefox */
}
.text-frame .text-editor::-webkit-scrollbar { display: none; } /* WebKit */
/* Move handle (top-left) visible only when active */
.frame-handle-move {
position: absolute;
top: 0;
left: 0;
width: 18px;
height: 18px;
display: none;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
color: var(--lumo-primary-color);
background: #fff;
border: 1px solid var(--lumo-primary-color);
border-radius: 4px;
box-shadow: var(--lumo-box-shadow-xs);
cursor: grab;
z-index: 2;
user-select: none;
transform: translate(-50%, -50%); /* center the handle on the top-left corner */
}
.canvas-frame.active .frame-handle-move { display: flex; }
.frame-handle-move:active { cursor: grabbing; }
/* bottom-right visual icon removed; native CSS resize handle remains */

View File

@@ -0,0 +1,56 @@
package de.assecutor.votianlt.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.Instant;
@Document(collection = "pdf_templates")
public class PdfTemplate {
@Id
private String id;
private String name;
private String dataJson; // raw JSON of canvas state
private Instant createdAt;
private Instant updatedAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDataJson() {
return dataJson;
}
public void setDataJson(String dataJson) {
this.dataJson = dataJson;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,42 @@
package de.assecutor.votianlt.pages.service;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.pdf.PdfWriter;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@Service
public class PDFGenerationService {
public byte[] generatePdfFromImage(byte[] pngBytes) {
try {
Document document = new Document(PageSize.A4);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter.getInstance(document, baos);
document.open();
Image image = Image.getInstance(pngBytes);
// Scale image to fit A4 with small margins
float margin = 20f;
float maxW = PageSize.A4.getWidth() - 2 * margin;
float maxH = PageSize.A4.getHeight() - 2 * margin;
image.scaleToFit(maxW, maxH);
// Center the image
float x = (PageSize.A4.getWidth() - image.getScaledWidth()) / 2f;
float y = (PageSize.A4.getHeight() - image.getScaledHeight()) / 2f;
image.setAbsolutePosition(x, y);
document.add(image);
document.close();
return baos.toByteArray();
} catch (DocumentException | IOException e) {
throw new RuntimeException("Fehler bei der PDF-Erzeugung", e);
}
}
}

View File

@@ -0,0 +1,32 @@
package de.assecutor.votianlt.pages.service;
import de.assecutor.votianlt.model.PdfTemplate;
import de.assecutor.votianlt.repository.PdfTemplateRepository;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Optional;
@Service
public class PdfTemplateService {
private final PdfTemplateRepository repository;
public PdfTemplateService(PdfTemplateRepository repository) {
this.repository = repository;
}
public PdfTemplate saveOrUpdate(String name, String dataJson) {
Optional<PdfTemplate> existing = repository.findByName(name);
PdfTemplate t = existing.orElseGet(PdfTemplate::new);
t.setName(name);
t.setDataJson(dataJson);
Instant now = Instant.now();
if (t.getCreatedAt() == null) t.setCreatedAt(now);
t.setUpdatedAt(now);
return repository.save(t);
}
public Optional<PdfTemplate> findByName(String name) {
return repository.findByName(name);
}
}

View File

@@ -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" +
"});"
);
}
}
}

View File

@@ -0,0 +1,10 @@
package de.assecutor.votianlt.repository;
import de.assecutor.votianlt.model.PdfTemplate;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.Optional;
public interface PdfTemplateRepository extends MongoRepository<PdfTemplate, String> {
Optional<PdfTemplate> findByName(String name);
}