BPMN-Vorschau im Browser, offline, auf Basis von bpmn-js
- werkzeuge/vorschau/vorschau.html: Anzeige per Drag & Drop oder Dateiauswahl, Zoom, Einpassen, Fehlermeldungen; funktioniert ohne Python direkt aus dem Dateisystem. - scripts/vorschau.py: erzeugt aus .bpmn oder Arbeitsschritt-.md eine in sich geschlossene Seite und öffnet sie; --stdin als Filter, etwa für einen Gitea-Renderer. Diagramm wird mit maskiertem '<' eingebettet, die Bibliothek unverändert und nur auf <script/</script geprüft. - bpmn-js 18.29.1 (navigated viewer + CSS + LICENSE) lokal eingebettet, npm-Integrität geprüft; lädt nichts aus dem Netz. Wasserzeichen bleibt laut Lizenzauflage sichtbar. - tests/test_vorschau.py: 11 Tests; Skill sn-bpmn, Anleitung, AGENTS.md, README, Leitfäden, Fallstricke und STATUS ergänzt. - Tests: python -m pytest -q -> 55 bestanden. Darstellung mit Edge headless geprüft (beide Beispiele, leere Seite, kaputte Datei). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,15 @@ in PICTURE geladen werden.
|
||||
|
||||
Um zu prüfen, ob `md_to_bpmn.py` selbst korrekt gerechnet hat (Struktur,
|
||||
Anschlusspunkte, Layout) – unabhängig davon, was ein Import-Werkzeug daraus
|
||||
macht – eignet sich ein originalgetreuer BPMN-Viewer:
|
||||
macht – eignet sich ein originalgetreuer BPMN-Viewer. Im Repository liegt
|
||||
einer, offline und ohne Datenübertragung:
|
||||
|
||||
```bash
|
||||
python scripts/vorschau.py entwuerfe/<name>.md # oder eine .bpmn
|
||||
```
|
||||
|
||||
Ohne Python: `werkzeuge/vorschau/vorschau.html` im Browser öffnen und die
|
||||
`.bpmn` hineinziehen (Details: `werkzeuge/vorschau/README.md`). Alternativ:
|
||||
|
||||
- **`demo.bpmn.io`** – im Browser, kein Install nötig: `.bpmn`-Datei per
|
||||
Drag & Drop reinziehen.
|
||||
|
||||
@@ -47,6 +47,10 @@ schreibst oder bearbeitest.
|
||||
Ergebnis kurz melden (Anzahl Schritte/Kanten/Lanes, Pfad der Datei) und
|
||||
anbieten, das Modell im Repository abzulegen (Schritt 5).
|
||||
|
||||
Vorschau im Browser, offline: `python scripts/vorschau.py entwuerfe/<name>.bpmn`
|
||||
(geht auch direkt mit der `.md`, schon vor der Freigabe in Schritt 3).
|
||||
Beim Anbieten der Vorschau dazusagen, dass PICTURE abweichen kann.
|
||||
|
||||
5. **Ablegen (nur auf Wunsch).** Ablauf und Regeln: `prozesse/README.md`
|
||||
(ADR 0003). Kurz: Branch `feature/<issue-nr>-<name>` von `main`, Ordner
|
||||
`prozesse/<name>/` mit `<name>.md` und Steckbrief `README.md` aus
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Bash(python scripts/setup_workspace.py:*)",
|
||||
"Bash(python scripts/pruefe_umlaute.py:*)",
|
||||
"Bash(python scripts/pruefe_prozesse.py:*)",
|
||||
"Bash(python scripts/vorschau.py:*)",
|
||||
"Bash(python -m pytest:*)"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,3 +11,6 @@
|
||||
*.pdf binary
|
||||
*.docx binary
|
||||
*.vsdx binary
|
||||
|
||||
# Eingebettete Fremdbibliothek (werkzeuge/vorschau/README.md): Bytes unverändert lassen, keine Diffs
|
||||
werkzeuge/vorschau/bpmn-js/** binary
|
||||
|
||||
@@ -51,7 +51,8 @@ Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gel
|
||||
- Rollen und Verantwortliche in Prozessmodellen als Funktion benennen („Sachbearbeitung Flurbereinigung"),
|
||||
nie als Person.
|
||||
- Keine Python-Abhängigkeiten außer der Standardbibliothek und `pytest` ohne Issue-Absprache und Eintrag in
|
||||
`fallstricke.md`.
|
||||
`fallstricke.md`. Einzige eingebettete Fremdbibliothek ist bpmn-js unter `werkzeuge/vorschau/bpmn-js/` –
|
||||
nicht von Hand ändern, Aktualisierung nach `werkzeuge/vorschau/README.md`.
|
||||
- Kein Umbau „nebenbei". Strukturänderungen nur über eigenes Issue.
|
||||
|
||||
### Allgemein
|
||||
@@ -86,6 +87,7 @@ Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gel
|
||||
- Tests: `python -m pytest -q`
|
||||
- Umlaute prüfen: `python scripts/pruefe_umlaute.py`
|
||||
- Prozessmodelle prüfen bzw. alle `.bpmn` neu erzeugen: `python scripts/pruefe_prozesse.py [--erzeugen]`
|
||||
- Diagramm im Browser ansehen (offline): `python scripts/vorschau.py <datei.bpmn|datei.md>`
|
||||
- Workspace einrichten oder aktualisieren: `python scripts/setup_workspace.py` (mit `--pull` für fast-forward)
|
||||
- Entwurf umwandeln: `python .agents/skills/sn-bpmn/md_to_bpmn.py entwuerfe/<name>.md entwuerfe/<name>.bpmn`
|
||||
|
||||
|
||||
@@ -58,7 +58,8 @@ Im Ordner `AG_Prozesse` Claude Code starten und zum Beispiel sagen:
|
||||
> *„Modelliere den Prozess aus `referenz/antragspruefung.docx` als BPMN."*
|
||||
|
||||
Der Skill `sn-bpmn` erzeugt daraus einen Entwurf `entwuerfe/<name>.md`, nennt die getroffenen Annahmen und wartet.
|
||||
Du prüfst und änderst den Entwurf fachlich, dann: *„passt so, wandle um"*. Heraus kommt `<name>.bpmn` für den
|
||||
Du prüfst und änderst den Entwurf fachlich — als Diagramm ansehen geht jederzeit mit
|
||||
`python scripts/vorschau.py entwuerfe/<name>.md` (offline im Browser) —, dann: *„passt so, wandle um"*. Heraus kommt `<name>.bpmn` für den
|
||||
Import in PICTURE. Syntax des Entwurfs: [`.agents/skills/sn-bpmn/Anleitung.md`](.agents/skills/sn-bpmn/Anleitung.md).
|
||||
|
||||
`referenz/` und `entwuerfe/` werden nicht versioniert — Quelldokumente bleiben auf deinem Rechner.
|
||||
@@ -97,7 +98,8 @@ Die vollständige, für Agenten verbindliche Fassung steht in [`AGENTS.md`](AGEN
|
||||
| [`AGENTS.md`](AGENTS.md) / `CLAUDE.md` | Anweisungen für Agenten (kanonisch ist `AGENTS.md`) |
|
||||
| [`workspace.toml`](workspace.toml) | Gitea-Organisation und Verzeichnis weiterer Repositories |
|
||||
| [`prozesse/`](prozesse/README.md) | Versionierte Prozessmodelle, ein Ordner je Prozess (ADR 0003) |
|
||||
| `scripts/` | `hallo.py`, `setup_workspace.py`, `gitea.py`, `pruefe_umlaute.py`, `pruefe_prozesse.py` |
|
||||
| `scripts/` | `hallo.py`, `setup_workspace.py`, `gitea.py`, `pruefe_umlaute.py`, `pruefe_prozesse.py`, `vorschau.py` |
|
||||
| [`werkzeuge/vorschau/`](werkzeuge/vorschau/README.md) | BPMN-Vorschau im Browser (bpmn-js, offline); `vorschau.html` auch ohne Python |
|
||||
| `.agents/skills/` | Skills, gespiegelt nach `.claude/skills/`; `sn-bpmn` enthält den Konverter |
|
||||
| `.claude/commands/` | Slash-Befehle für Claude Code (`/hallo`) |
|
||||
| `docs/status/` | **Aktueller Stand** — immer zuerst lesen |
|
||||
|
||||
@@ -32,6 +32,8 @@ flowchart TD
|
||||
|
||||
- **Der Agent wartet nach dem Entwurf.** Er wandelt erst um, wenn du ausdrücklich zustimmst. Das ist Absicht: die
|
||||
fachliche Richtigkeit verantwortest du, nicht der Agent.
|
||||
- **Ansehen statt nur lesen.** `python scripts/vorschau.py entwuerfe/<name>.md` zeigt den Entwurf als Diagramm im
|
||||
Browser, offline. Ohne Python: `werkzeuge/vorschau/vorschau.html` öffnen und die `.bpmn` hineinziehen.
|
||||
- **Annahmen ernst nehmen.** Der Agent nennt, wo er Schrittgrenzen, Verzweigungen oder Verantwortliche geraten hat.
|
||||
Genau dort liegen die Fehler.
|
||||
- **Rollen statt Personen.** Verantwortliche immer als Funktion („Sachbearbeitung"), nie mit Namen.
|
||||
@@ -129,7 +131,8 @@ kleine PRs machen.
|
||||
|
||||
**Review**
|
||||
|
||||
- [ ] Fachlich richtig? (BPMN-Regeln, Begriffe; bei Modellen: Diff der `.md` lesen, nicht der `.bpmn`)
|
||||
- [ ] Fachlich richtig? (BPMN-Regeln, Begriffe; bei Modellen: Diff der `.md` lesen, nicht der `.bpmn`,
|
||||
und das Diagramm mit `python scripts/vorschau.py` ansehen)
|
||||
- [ ] Tests vorhanden und sinnvoll (nicht nur angepasst)?
|
||||
- [ ] Umfang überschaubar (< 400 Zeilen)?
|
||||
- [ ] Nach dem Merge: `STATUS.md`
|
||||
|
||||
@@ -116,8 +116,8 @@ Die Auto-Memory ist lokal und wird nicht geteilt; Teilbares gehört nach `docs/`
|
||||
| Python | `ms-python.python` | Syntax, Interpreterwahl, Debugger |
|
||||
| Markdown Preview Mermaid Support | `bierner.markdown-mermaid` | zeigt Mermaid-Diagramme in der Vorschau |
|
||||
|
||||
Zum Ansehen einer `.bpmn` ohne PICTURE eignet sich der Camunda Modeler (Desktop, offline) oder `demo.bpmn.io`,
|
||||
siehe `.agents/skills/sn-bpmn/Anleitung.md` Abschnitt „Diagramm vorab prüfen“.
|
||||
Zum Ansehen einer `.bpmn` ohne PICTURE ist keine Erweiterung nötig: `python scripts/vorschau.py <datei>` öffnet
|
||||
sie im Browser, offline (`werkzeuge/vorschau/README.md`).
|
||||
|
||||
### 4.2 Persönliche Einstellungen
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern i
|
||||
|
||||
## Erledigt
|
||||
|
||||
- 23.09.2026 **BPMN-Vorschau** im Browser, offline: `scripts/vorschau.py` und `werkzeuge/vorschau/vorschau.html`
|
||||
auf Basis von bpmn-js 18.29.1 (lokal eingebettet, MIT mit Wasserzeichen-Auflage). Anzeige direkt in Gitea wäre
|
||||
Sache der Server-Administration (externer Renderer), Issue-Entwurf liegt vor.
|
||||
|
||||
- 23.09.2026 **Ablage der Prozessmodelle** in `prozesse/<name>/` mit Steckbrief, `.md` und erzeugter `.bpmn`
|
||||
(ADR 0003). `scripts/pruefe_prozesse.py` prüft Struktur, Status und Aktualität der `.bpmn`, auch als Test.
|
||||
Die in `Anleitung.md` genannten, aber nie committeten Beispiele liegen jetzt unter
|
||||
|
||||
@@ -5,6 +5,13 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
|
||||
|
||||
## BPMN und PICTURE (Skill sn-bpmn, md_to_bpmn.py)
|
||||
|
||||
- **Das bpmn.io-Wasserzeichen ist Lizenzauflage.** bpmn-js steht unter MIT, verlangt aber, dass das Wasserzeichen
|
||||
unten rechts sichtbar bleibt und nicht überdeckt wird. In `werkzeuge/vorschau/vorschau.html` deshalb nichts in die
|
||||
untere rechte Ecke der Zeichenfläche legen. (bpmn-js `LICENSE`, 23.09.2026)
|
||||
- **Diagramme nie roh in ein `<script>` schreiben.** Eine Beschriftung mit `</script>` würde den Skriptblock beenden.
|
||||
`scripts/vorschau.py` maskiert jedes `<` im Diagramm als `\u003c`; die Bibliothek selbst wird nicht verändert,
|
||||
sondern nur auf `<script`/`</script` geprüft. (23.09.2026)
|
||||
|
||||
- **Jede Änderung an der Ausgabe von `md_to_bpmn.py` macht die abgelegten `.bpmn` rot.** Der Test in
|
||||
`tests/test_pruefe_prozesse.py` vergleicht jede `.bpmn` unter `prozesse/` und in `beispiele/` byte-genau mit der
|
||||
Umwandlung ihrer `.md`. Nach einer Konverteränderung `python scripts/pruefe_prozesse.py --erzeugen` ausführen und
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Zeigt eine BPMN-Datei oder eine Arbeitsschritt-.md als Diagramm im Browser – offline, ohne PICTURE.
|
||||
|
||||
Grundlage ist werkzeuge/vorschau/vorschau.html mit der lokal eingebetteten Bibliothek bpmn-js (bpmn.io).
|
||||
Das Skript erzeugt daraus eine einzelne, in sich geschlossene HTML-Datei (Bibliothek, Stile und Diagramm
|
||||
eingebettet) und öffnet sie im Standardbrowser. Es wird nichts übertragen; die Seite lädt nichts aus dem Netz.
|
||||
|
||||
Aufrufe (aus dem Workspace-Root):
|
||||
python scripts/vorschau.py prozesse/antrag-pruefen/antrag-pruefen.bpmn
|
||||
python scripts/vorschau.py entwuerfe/antrag-pruefen.md # .md wird vorher umgewandelt
|
||||
python scripts/vorschau.py <datei> --ausgabe vorschau.html --nicht-oeffnen
|
||||
python scripts/vorschau.py --stdin < datei.bpmn > vorschau.html # Filter, z. B. als Gitea-Renderer
|
||||
|
||||
Ohne Python: werkzeuge/vorschau/vorschau.html direkt im Browser öffnen und eine .bpmn hineinziehen.
|
||||
|
||||
Exit-Code: 0 = in Ordnung, 1 = Datei fehlt oder lässt sich nicht umwandeln.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VORSCHAU = ROOT / "werkzeuge" / "vorschau"
|
||||
VORLAGE = VORSCHAU / "vorschau.html"
|
||||
SKILL = ROOT / ".agents" / "skills" / "sn-bpmn"
|
||||
|
||||
# Stellen in vorschau.html, die beim Einbetten ersetzt werden. Ein Test prüft, dass es sie gibt.
|
||||
EINBETTUNGEN = {
|
||||
'<link rel="stylesheet" href="bpmn-js/diagram-js.css">': ("style", "bpmn-js/diagram-js.css"),
|
||||
'<link rel="stylesheet" href="bpmn-js/bpmn-js.css">': ("style", "bpmn-js/bpmn-js.css"),
|
||||
'<script src="bpmn-js/bpmn-navigated-viewer.production.min.js"></script>':
|
||||
("script", "bpmn-js/bpmn-navigated-viewer.production.min.js"),
|
||||
}
|
||||
PLATZHALTER_DIAGRAMM = "const EINGEBETTET = null;"
|
||||
PLATZHALTER_NAME = "const DATEINAME = null;"
|
||||
|
||||
|
||||
# Sequenzen, die einen eingebetteten <script>- bzw. <style>-Block vorzeitig beenden oder den HTML-Parser in
|
||||
# den Sonderzustand für "<!--" im Skript bringen würden. Die Bibliothek wird unverändert eingebettet und deshalb
|
||||
# nur geprüft; nach einem Update von bpmn-js schlägt baue_html() sonst laut fehl, statt eine kaputte Seite zu liefern.
|
||||
VERBOTEN = {"script": ("</script", "<script"), "style": ("</style",)}
|
||||
|
||||
|
||||
def _js_wert(text: str | None) -> str:
|
||||
"""Als JavaScript-Literal, sicher innerhalb von <script>: jedes '<' wird als \\u003c geschrieben."""
|
||||
return json.dumps(text, ensure_ascii=False).replace("<", "\\u003c")
|
||||
|
||||
|
||||
def baue_html(bpmn_xml: str, name: str | None = None) -> str:
|
||||
"""Eigenständige Vorschau-Seite mit eingebetteter Bibliothek und eingebettetem Diagramm."""
|
||||
html = VORLAGE.read_text(encoding="utf-8")
|
||||
for fundstelle, (art, datei) in EINBETTUNGEN.items():
|
||||
if fundstelle not in html:
|
||||
raise RuntimeError(f"{VORLAGE.name}: '{fundstelle}' fehlt – Vorlage und scripts/vorschau.py passen nicht zusammen")
|
||||
inhalt = (VORSCHAU / datei).read_text(encoding="utf-8")
|
||||
for sequenz in VERBOTEN[art]:
|
||||
if sequenz in inhalt.lower():
|
||||
raise RuntimeError(f"{datei} enthält '{sequenz}' und kann nicht sicher eingebettet werden")
|
||||
html = html.replace(fundstelle, f"<{art}>\n{inhalt}\n</{art}>")
|
||||
for platzhalter in (PLATZHALTER_DIAGRAMM, PLATZHALTER_NAME):
|
||||
if platzhalter not in html:
|
||||
raise RuntimeError(f"{VORLAGE.name}: Platzhalter '{platzhalter}' fehlt")
|
||||
html = html.replace(PLATZHALTER_DIAGRAMM, f"const EINGEBETTET = {_js_wert(bpmn_xml)};")
|
||||
return html.replace(PLATZHALTER_NAME, f"const DATEINAME = {_js_wert(name)};")
|
||||
|
||||
|
||||
def lies_diagramm(pfad: Path) -> str:
|
||||
"""BPMN-XML aus einer .bpmn lesen oder aus einer Arbeitsschritt-.md erzeugen."""
|
||||
if pfad.suffix.lower() == ".md":
|
||||
sys.path.insert(0, str(SKILL))
|
||||
import md_to_bpmn # noqa: PLC0415 (liegt im Skill-Ordner, nicht als Paket installiert)
|
||||
|
||||
meta, steps, order, externals = md_to_bpmn.parse_markdown(pfad.read_text(encoding="utf-8"))
|
||||
return md_to_bpmn.prettify(md_to_bpmn.build_bpmn(meta, steps, order, externals)).decode("utf-8")
|
||||
return pfad.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("datei", nargs="?", help=".bpmn oder Arbeitsschritt-.md")
|
||||
ap.add_argument("--ausgabe", help="HTML-Datei (Standard: temporäre Datei)")
|
||||
ap.add_argument("--nicht-oeffnen", action="store_true", help="nur erzeugen, keinen Browser öffnen")
|
||||
ap.add_argument("--stdin", action="store_true", help="BPMN-XML von stdin lesen, HTML nach stdout schreiben")
|
||||
args = ap.parse_args()
|
||||
# Windows-Konsolen und Pipes laufen oft mit cp1252; Meldungen mit Umlauten wären sonst verstümmelt.
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
if args.stdin:
|
||||
xml = sys.stdin.buffer.read().decode("utf-8")
|
||||
sys.stdout.buffer.write(baue_html(xml).encode("utf-8"))
|
||||
return 0
|
||||
if not args.datei:
|
||||
ap.error("Datei angeben oder --stdin verwenden")
|
||||
|
||||
pfad = Path(args.datei)
|
||||
if not pfad.is_file():
|
||||
print(f"Datei nicht gefunden: {pfad}")
|
||||
return 1
|
||||
try:
|
||||
xml = lies_diagramm(pfad)
|
||||
except ValueError as fehler:
|
||||
print(f"{pfad} lässt sich nicht umwandeln: {fehler}")
|
||||
return 1
|
||||
|
||||
if args.ausgabe:
|
||||
ziel = Path(args.ausgabe)
|
||||
else:
|
||||
ziel = Path(tempfile.gettempdir()) / "bpmn-vorschau" / f"{pfad.stem}.html"
|
||||
ziel.parent.mkdir(exist_ok=True)
|
||||
ziel.write_text(baue_html(xml, pfad.name), encoding="utf-8")
|
||||
print(f"Vorschau: {ziel}")
|
||||
if not args.nicht_oeffnen:
|
||||
webbrowser.open(ziel.resolve().as_uri())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Tests für scripts/vorschau.py und die eingebettete bpmn-js-Vorschau – reines Python/pytest, ohne Browser."""
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import vorschau # noqa: E402
|
||||
|
||||
BEISPIEL_MD = ROOT / ".agents" / "skills" / "sn-bpmn" / "beispiele" / "beispiel_prozess.md"
|
||||
BEISPIEL_BPMN = BEISPIEL_MD.with_suffix(".bpmn")
|
||||
|
||||
|
||||
def test_vorlage_verweist_nur_auf_vorhandene_lokale_dateien():
|
||||
"""Prüft, dass vorschau.html nur Dateien unter werkzeuge/vorschau/ einbindet, die es gibt, und nichts aus dem Netz."""
|
||||
html = vorschau.VORLAGE.read_text(encoding="utf-8")
|
||||
verweise = re.findall(r'(?:src|href)="([^"]+)"', html)
|
||||
assert verweise, "keine eingebundenen Dateien gefunden"
|
||||
for verweis in verweise:
|
||||
assert not re.match(r"^[a-z]+:", verweis), f"externer Verweis: {verweis}"
|
||||
assert (vorschau.VORSCHAU / verweis).is_file(), f"fehlt: {verweis}"
|
||||
|
||||
|
||||
def test_lizenz_von_bpmn_js_liegt_bei():
|
||||
"""Prüft, dass die Lizenz von bpmn-js neben der eingebetteten Bibliothek liegt (MIT-Auflage)."""
|
||||
lizenz = (vorschau.VORSCHAU / "bpmn-js" / "LICENSE").read_text(encoding="utf-8")
|
||||
assert "watermark" in lizenz
|
||||
|
||||
|
||||
def test_erzeugte_seite_ist_in_sich_geschlossen():
|
||||
"""Prüft, dass die erzeugte Seite Bibliothek und Stile eingebettet hat und keine Datei mehr nachlädt."""
|
||||
html = vorschau.baue_html(BEISPIEL_BPMN.read_text(encoding="utf-8"), BEISPIEL_BPMN.name)
|
||||
assert not re.search(r"<script[^>]*\bsrc=", html)
|
||||
assert "<link" not in html
|
||||
assert "BpmnJS" in html
|
||||
assert "const EINGEBETTET = null;" not in html
|
||||
assert '"beispiel_prozess.bpmn"' in html
|
||||
|
||||
|
||||
def test_diagramm_wird_als_javascript_zeichenkette_eingebettet():
|
||||
"""Prüft, dass der Inhalt der .bpmn verlustfrei als JSON-Zeichenkette mit maskiertem '<' in der Seite landet."""
|
||||
xml = '<definitions id="x">Ä&</definitions>'
|
||||
html = vorschau.baue_html(xml, "x.bpmn")
|
||||
wert = re.search(r"const EINGEBETTET = (.*);", html).group(1)
|
||||
assert "<" not in wert
|
||||
assert json.loads(wert) == xml
|
||||
|
||||
|
||||
def test_script_ende_im_diagramm_bricht_die_seite_nicht_auf():
|
||||
"""Prüft, dass '</script>' in einer Beschriftung nicht als Ende des Skriptblocks wirkt."""
|
||||
html = vorschau.baue_html('<task name="</script><script>alert(1)</script>"/>', "x.bpmn")
|
||||
assert "alert(1)" in html
|
||||
assert "<script>alert(1)" not in html
|
||||
assert html.lower().count("</script") == html.lower().count("<script")
|
||||
|
||||
|
||||
def test_bibliothek_mit_script_ende_wird_abgelehnt(tmp_path, monkeypatch):
|
||||
"""Prüft, dass eine (etwa nach einem Update) nicht sicher einbettbare Bibliothek einen Fehler auslöst."""
|
||||
kopie = tmp_path / "vorschau"
|
||||
shutil.copytree(vorschau.VORSCHAU, kopie)
|
||||
js = kopie / "bpmn-js" / "bpmn-navigated-viewer.production.min.js"
|
||||
js.write_text(js.read_text(encoding="utf-8") + '\nvar x = "</script>";', encoding="utf-8")
|
||||
monkeypatch.setattr(vorschau, "VORSCHAU", kopie)
|
||||
monkeypatch.setattr(vorschau, "VORLAGE", kopie / "vorschau.html")
|
||||
with pytest.raises(RuntimeError, match="</script"):
|
||||
vorschau.baue_html("<x/>")
|
||||
|
||||
|
||||
def test_md_wird_vor_der_vorschau_umgewandelt():
|
||||
"""Prüft, dass eine Arbeitsschritt-.md dasselbe XML liefert wie ihre erzeugte .bpmn."""
|
||||
erwartet = BEISPIEL_BPMN.read_text(encoding="utf-8").replace("\r\n", "\n")
|
||||
assert vorschau.lies_diagramm(BEISPIEL_MD).replace("\r\n", "\n") == erwartet
|
||||
|
||||
|
||||
def test_ausgabe_in_datei_ohne_browser(tmp_path):
|
||||
"""Prüft, dass --ausgabe mit --nicht-oeffnen eine Seite schreibt und Exit-Code 0 liefert."""
|
||||
ziel = tmp_path / "v.html"
|
||||
ergebnis = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(BEISPIEL_MD),
|
||||
"--ausgabe", str(ziel), "--nicht-oeffnen"],
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
)
|
||||
assert ergebnis.returncode == 0, ergebnis.stdout + ergebnis.stderr
|
||||
assert "BpmnJS" in ziel.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_stdin_modus_liest_xml_und_schreibt_html():
|
||||
"""Prüft, dass --stdin als Filter arbeitet (Grundlage für einen Gitea-Renderer)."""
|
||||
ergebnis = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), "--stdin"],
|
||||
input=BEISPIEL_BPMN.read_bytes(), capture_output=True,
|
||||
)
|
||||
assert ergebnis.returncode == 0, ergebnis.stderr.decode("utf-8", "replace")
|
||||
html = ergebnis.stdout.decode("utf-8")
|
||||
assert html.startswith("<!DOCTYPE html>") and "Antrag erfassen" in html
|
||||
|
||||
|
||||
def test_fehlende_datei_gibt_exit_code_1(tmp_path):
|
||||
"""Prüft, dass eine nicht vorhandene Eingabedatei sauber mit Exit-Code 1 endet."""
|
||||
ergebnis = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(tmp_path / "gibt-es-nicht.bpmn"),
|
||||
"--nicht-oeffnen"],
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
)
|
||||
assert ergebnis.returncode == 1
|
||||
assert "nicht gefunden" in ergebnis.stdout
|
||||
|
||||
|
||||
def test_nicht_umwandelbare_md_gibt_exit_code_1(tmp_path):
|
||||
"""Prüft, dass eine .md mit Syntaxfehler als Meldung statt als Absturz endet."""
|
||||
md = tmp_path / "kaputt.md"
|
||||
md.write_text("S1 Start: A\nS1 Ende: B\n", encoding="utf-8")
|
||||
ergebnis = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(md), "--nicht-oeffnen"],
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
)
|
||||
assert ergebnis.returncode == 1
|
||||
assert "lässt sich nicht umwandeln" in ergebnis.stdout
|
||||
@@ -0,0 +1,49 @@
|
||||
# BPMN-Vorschau
|
||||
|
||||
Zeigt eine `.bpmn`-Datei als Diagramm im Browser — offline, ohne PICTURE, ohne dass die Datei den Rechner verlässt.
|
||||
Grundlage ist [bpmn-js](https://github.com/bpmn-io/bpmn-js), die Bibliothek hinter bpmn.io, lokal eingebettet.
|
||||
|
||||
## Benutzen
|
||||
|
||||
**Mit Python** (aus dem Workspace-Root) — öffnet die Vorschau direkt im Browser, auch für einen Entwurf:
|
||||
|
||||
```bash
|
||||
python scripts/vorschau.py prozesse/antrag-pruefen/antrag-pruefen.bpmn
|
||||
python scripts/vorschau.py entwuerfe/antrag-pruefen.md # .md wird vorher umgewandelt
|
||||
```
|
||||
|
||||
**Ohne Python:** `werkzeuge/vorschau/vorschau.html` im Browser öffnen (Doppelklick) und eine `.bpmn` hineinziehen
|
||||
oder über „Datei öffnen …“ wählen. Das funktioniert auch mit einer aus Gitea heruntergeladenen Datei.
|
||||
|
||||
Mausrad zoomt, Ziehen verschiebt, „Einpassen“ zeigt das ganze Diagramm.
|
||||
|
||||
## Was die Vorschau zeigt — und was nicht
|
||||
|
||||
Sie zeigt, was **in der Datei steht**: Elemente, Lanes, Kanten mit genau den Anschlusspunkten, die `md_to_bpmn.py`
|
||||
berechnet hat. Damit eignet sie sich, um Entwürfe vor der Freigabe und Modelländerungen im Review anzusehen.
|
||||
PICTURE kann dieselbe Datei anders darstellen (eigene Schriften, eigene Behandlung von Anschlusspunkten). Der Import
|
||||
in PICTURE bleibt deshalb der letzte Nachweis.
|
||||
|
||||
## Eingebettete Bibliothek
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Paket | `bpmn-js` 18.29.1 (npm), Datei `dist/bpmn-navigated-viewer.production.min.js` und `dist/assets/*.css` |
|
||||
| Quelle | `https://registry.npmjs.org/bpmn-js/-/bpmn-js-18.29.1.tgz` |
|
||||
| Integrität (npm) | `sha512-7M3O9LeHbV2ZW9xLXap8CMmIjR+iXWqiXrz7Rt1hE+DqLGhULuwOIgTsQGtXPGVUCrnrk4L38NVlc5R8thOHJw==` |
|
||||
| Lizenz | MIT mit Auflage, siehe `bpmn-js/LICENSE` |
|
||||
|
||||
**Lizenzauflage:** Das bpmn.io-Wasserzeichen unten rechts im Diagramm darf nicht entfernt, verändert oder von
|
||||
anderen Elementen überdeckt werden. Bedienelemente deshalb nie in die untere rechte Ecke der Zeichenfläche legen.
|
||||
|
||||
Die Bibliothek lädt nichts aus dem Netz nach; die Adressen darin sind XML-Namensräume und der Link des Wasserzeichens.
|
||||
Das Font-Paket `bpmn-font` wird nur vom Editor gebraucht und fehlt bewusst.
|
||||
|
||||
## Aktualisieren
|
||||
|
||||
1. Neue Version bei npm nachsehen, Tarball laden und die `integrity` aus `https://registry.npmjs.org/bpmn-js/<version>`
|
||||
gegen die Prüfsumme des geladenen Archivs prüfen.
|
||||
2. Die vier Dateien in `bpmn-js/` ersetzen (`LICENSE` auf geänderte Auflagen lesen).
|
||||
3. `python -m pytest -q` — `scripts/vorschau.py` lehnt eine Bibliothek ab, die `<script`/`</script` enthält und damit
|
||||
nicht sicher eingebettet werden kann.
|
||||
4. Eine Vorschau erzeugen und im Browser ansehen; Tabelle oben nachziehen. Eigener Pull Request.
|
||||
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2014-present Camunda Services GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
The source code responsible for displaying the bpmn.io project watermark that
|
||||
links back to https://bpmn.io as part of rendered diagrams MUST NOT be
|
||||
removed or changed. When this software is being used in a website or application,
|
||||
the watermark must stay fully visible and not visually overlapped by other elements.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,160 @@
|
||||
.bjs-container {
|
||||
--bjs-font-family: Arial, sans-serif;
|
||||
|
||||
--color-grey-225-10-15: hsl(225, 10%, 15%);
|
||||
--color-grey-225-10-35: hsl(225, 10%, 35%);
|
||||
--color-grey-225-10-55: hsl(225, 10%, 55%);
|
||||
--color-grey-225-10-75: hsl(225, 10%, 75%);
|
||||
--color-grey-225-10-80: hsl(225, 10%, 80%);
|
||||
--color-grey-225-10-85: hsl(225, 10%, 85%);
|
||||
--color-grey-225-10-90: hsl(225, 10%, 90%);
|
||||
--color-grey-225-10-95: hsl(225, 10%, 95%);
|
||||
--color-grey-225-10-97: hsl(225, 10%, 97%);
|
||||
|
||||
--color-blue-205-100-40: hsl(205, 100%, 40%);
|
||||
--color-blue-205-100-45: hsl(205, 100%, 45%);
|
||||
--color-blue-205-100-45-opacity-30: hsla(205, 100%, 45%, 30%);
|
||||
--color-blue-205-100-50: hsl(205, 100%, 50%);
|
||||
--color-blue-205-100-95: hsl(205, 100%, 95%);
|
||||
|
||||
--color-green-150-86-44: hsl(150, 86%, 44%);
|
||||
|
||||
--color-red-360-100-40: hsl(360, 100%, 40%);
|
||||
--color-red-360-100-45: hsl(360, 100%, 45%);
|
||||
--color-red-360-100-92: hsl(360, 100%, 92%);
|
||||
--color-red-360-100-97: hsl(360, 100%, 97%);
|
||||
|
||||
--color-white: hsl(0, 0%, 100%);
|
||||
--color-black: hsl(0, 0%, 0%);
|
||||
--color-black-opacity-05: hsla(0, 0%, 0%, 5%);
|
||||
--color-black-opacity-10: hsla(0, 0%, 0%, 10%);
|
||||
|
||||
--accent-color: var(--color-blue-205-100-40);
|
||||
|
||||
--breadcrumbs-font-family: var(--bjs-font-family);
|
||||
--breadcrumbs-item-color: var(--accent-color);
|
||||
--breadcrumbs-arrow-color: var(--color-black);
|
||||
--breadcrumbs-background-color: var(--color-grey-225-10-97);
|
||||
--breadcrumbs-border-color: var(--color-grey-225-10-75);
|
||||
--breadcrumbs-focus-outline-color: var(--accent-color);
|
||||
--drilldown-fill-color: var(--color-white);
|
||||
--drilldown-background-color: var(--accent-color);
|
||||
--drilldown-focus-outline-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs {
|
||||
position: absolute;
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
padding: 5px 10px;
|
||||
margin: 0px;
|
||||
font-family: var(--breadcrumbs-font-family);
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
background-color: var(--breadcrumbs-background-color);
|
||||
border: solid 1px var(--breadcrumbs-border-color);
|
||||
border-radius: 2px;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs-shown .bjs-breadcrumbs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.djs-palette-shown .bjs-breadcrumbs {
|
||||
left: 90px;
|
||||
}
|
||||
|
||||
.djs-palette-shown.djs-palette-two-column .bjs-breadcrumbs {
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs a.bjs-crumb {
|
||||
cursor: pointer;
|
||||
color: var(--breadcrumbs-item-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs a.bjs-crumb:focus-visible {
|
||||
outline: solid 1px var(--breadcrumbs-focus-outline-color);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs li:not(:first-child)::before {
|
||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" /><path d="M0 0h24v24H0z" fill="none" /></svg>');
|
||||
padding: 0 8px;
|
||||
color: var(--breadcrumbs-arrow-color);
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs .bjs-crumb {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bjs-drilldown {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
padding: 0px;
|
||||
margin-left: -20px;
|
||||
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
|
||||
fill: var(--drilldown-fill-color);
|
||||
background-color: var(--drilldown-background-color);
|
||||
}
|
||||
|
||||
.bjs-drilldown:focus-visible {
|
||||
outline: solid 1px var(--drilldown-focus-outline-color);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.bjs-drilldown-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selected .bjs-drilldown-empty {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-results {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-body [data-group] + [data-group] {
|
||||
border-left: 1px solid var(--popup-border-color);
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] [data-group="align"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-body .entry {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-body .entry:not(:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-entry-icon {
|
||||
display: block;
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BPMN-Vorschau</title>
|
||||
<!-- bpmn-js (bpmn.io), lokal eingebettet – siehe README.md. Nichts wird aus dem Netz geladen. -->
|
||||
<link rel="stylesheet" href="bpmn-js/diagram-js.css">
|
||||
<link rel="stylesheet" href="bpmn-js/bpmn-js.css">
|
||||
<style>
|
||||
:root {
|
||||
--hintergrund: #ffffff;
|
||||
--leiste: #f3f5f7;
|
||||
--rand: #d0d7de;
|
||||
--text: #1f2328;
|
||||
--gedaempft: #59636e;
|
||||
--akzent: #0b6bcb;
|
||||
--fehler: #b42318;
|
||||
--warnung: #9a6700;
|
||||
}
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
display: flex; flex-direction: column;
|
||||
font-family: "Segoe UI", system-ui, sans-serif; font-size: 14px;
|
||||
color: var(--text); background: var(--hintergrund);
|
||||
}
|
||||
header {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
||||
padding: 8px 16px; background: var(--leiste); border-bottom: 1px solid var(--rand);
|
||||
}
|
||||
header h1 { font-size: 15px; font-weight: 600; margin: 0 12px 0 0; }
|
||||
button, label.knopf {
|
||||
font: inherit; padding: 4px 10px; border: 1px solid var(--rand); border-radius: 6px;
|
||||
background: #fff; color: var(--text); cursor: pointer;
|
||||
}
|
||||
button:hover, label.knopf:hover { border-color: var(--akzent); }
|
||||
button:disabled { opacity: .5; cursor: default; }
|
||||
#datei { display: none; }
|
||||
#dateiname { color: var(--gedaempft); overflow-wrap: anywhere; }
|
||||
#meldung { flex-basis: 100%; margin: 0; }
|
||||
#meldung:empty { display: none; }
|
||||
#meldung.fehler { color: var(--fehler); }
|
||||
#meldung.warnung { color: var(--warnung); }
|
||||
/* Zeichenfläche: das bpmn.io-Wasserzeichen unten rechts muss laut Lizenz sichtbar und unverdeckt bleiben. */
|
||||
main { position: relative; flex: 1; min-height: 0; }
|
||||
#flaeche { position: absolute; inset: 0; }
|
||||
#hinweis {
|
||||
position: absolute; inset: 24px 24px 64px 24px; display: flex; align-items: center; justify-content: center;
|
||||
text-align: center; color: var(--gedaempft); border: 2px dashed var(--rand); border-radius: 12px;
|
||||
padding: 16px; pointer-events: none;
|
||||
}
|
||||
main.ziehen #hinweis { border-color: var(--akzent); color: var(--akzent); }
|
||||
main.geladen #hinweis { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>BPMN-Vorschau</h1>
|
||||
<label class="knopf" for="datei">Datei öffnen …</label>
|
||||
<input type="file" id="datei" accept=".bpmn,.xml">
|
||||
<button type="button" id="einpassen" disabled>Einpassen</button>
|
||||
<button type="button" id="groesser" disabled title="Vergrößern">+</button>
|
||||
<button type="button" id="kleiner" disabled title="Verkleinern">−</button>
|
||||
<span id="dateiname"></span>
|
||||
<p id="meldung" role="status"></p>
|
||||
</header>
|
||||
<main id="bereich">
|
||||
<div id="flaeche"></div>
|
||||
<div id="hinweis">.bpmn-Datei hierher ziehen oder über „Datei öffnen …“ wählen.<br>
|
||||
Die Datei wird nur in diesem Browser angezeigt und nirgendwohin übertragen.</div>
|
||||
</main>
|
||||
<script src="bpmn-js/bpmn-navigated-viewer.production.min.js"></script>
|
||||
<script>
|
||||
// Wird von scripts/vorschau.py durch den Inhalt einer .bpmn und ihren Namen ersetzt.
|
||||
const EINGEBETTET = null;
|
||||
const DATEINAME = null;
|
||||
|
||||
const bereich = document.getElementById('bereich');
|
||||
const meldung = document.getElementById('meldung');
|
||||
const knoepfe = ['einpassen', 'groesser', 'kleiner'].map(id => document.getElementById(id));
|
||||
const viewer = new BpmnJS({ container: '#flaeche' });
|
||||
const canvas = () => viewer.get('canvas');
|
||||
|
||||
function melde(text, art) {
|
||||
meldung.textContent = text || '';
|
||||
meldung.className = art || '';
|
||||
}
|
||||
|
||||
async function zeige(xml, name) {
|
||||
melde('');
|
||||
try {
|
||||
const { warnings } = await viewer.importXML(xml);
|
||||
canvas().zoom('fit-viewport', 'auto');
|
||||
bereich.classList.add('geladen');
|
||||
knoepfe.forEach(k => { k.disabled = false; });
|
||||
document.getElementById('dateiname').textContent = name || '';
|
||||
document.title = name ? `${name} – BPMN-Vorschau` : 'BPMN-Vorschau';
|
||||
if (warnings.length) {
|
||||
melde(`${warnings.length} Warnung(en) beim Einlesen: ` + warnings.map(w => w.message).join(' | '), 'warnung');
|
||||
}
|
||||
} catch (fehler) {
|
||||
bereich.classList.remove('geladen');
|
||||
knoepfe.forEach(k => { k.disabled = true; });
|
||||
melde(`Die Datei ${name || ''} lässt sich nicht als BPMN lesen: ${fehler.message}`, 'fehler');
|
||||
}
|
||||
}
|
||||
|
||||
function lies(datei) {
|
||||
if (!datei) return;
|
||||
datei.text().then(xml => zeige(xml, datei.name));
|
||||
}
|
||||
|
||||
document.getElementById('datei').addEventListener('change', e => lies(e.target.files[0]));
|
||||
document.getElementById('einpassen').addEventListener('click', () => canvas().zoom('fit-viewport', 'auto'));
|
||||
document.getElementById('groesser').addEventListener('click', () => canvas().zoom(canvas().zoom() * 1.25));
|
||||
document.getElementById('kleiner').addEventListener('click', () => canvas().zoom(canvas().zoom() / 1.25));
|
||||
|
||||
['dragenter', 'dragover'].forEach(art => document.addEventListener(art, e => {
|
||||
e.preventDefault(); bereich.classList.add('ziehen');
|
||||
}));
|
||||
['dragleave', 'drop'].forEach(art => document.addEventListener(art, e => {
|
||||
e.preventDefault(); bereich.classList.remove('ziehen');
|
||||
}));
|
||||
document.addEventListener('drop', e => lies(e.dataTransfer.files[0]));
|
||||
|
||||
if (EINGEBETTET) zeige(EINGEBETTET, DATEINAME);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user