Erweiterungen
This commit is contained in:
32
src/main/bundles/README.md
Normal file
32
src/main/bundles/README.md
Normal 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
BIN
src/main/bundles/dev.bundle
Normal file
Binary file not shown.
182
src/main/frontend/styles/pdf-builder.css
Normal file
182
src/main/frontend/styles/pdf-builder.css
Normal 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 */
|
||||||
56
src/main/java/de/assecutor/votianlt/model/PdfTemplate.java
Normal file
56
src/main/java/de/assecutor/votianlt/model/PdfTemplate.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" +
|
||||||
|
"});"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user