Compare commits
4 Commits
v1.9.0
...
5919a55cbf
| Author | SHA1 | Date | |
|---|---|---|---|
| 5919a55cbf | |||
| 7edaf8c00b | |||
| 61a971ccc0 | |||
| 471cfbf98b |
@@ -0,0 +1,34 @@
|
||||
---
|
||||
context: default
|
||||
phase: quick-auftraege-1.9.x (keine GSD-Phase)
|
||||
task: 0
|
||||
total_tasks: 0
|
||||
status: paused
|
||||
last_updated: 2026-09-30T18:00:00.000Z
|
||||
---
|
||||
|
||||
# BLOCKING CONSTRAINTS — Read Before Anything Else
|
||||
|
||||
- [ ] CONSTRAINT: live NICHT pushen/taggen, bis der User es verlangt.
|
||||
- [ ] CONSTRAINT: Gebündelt pushen, nicht nach jeder Kleinigkeit.
|
||||
- [ ] CONSTRAINT: Browser-Prüfungen im Dunkelmodus.
|
||||
- [ ] CONSTRAINT: Nie Wichtiges (Logo, Text) nur in Mailbilder packen – OWA zeigt eingebettete Bilder nicht.
|
||||
|
||||
<current_state>
|
||||
main = live = v1.9.0 (a257bc3), CI + Release grün. alpha zuletzt 714f731 (Inhalt identisch). Arbeitsbaum sauber.
|
||||
</current_state>
|
||||
|
||||
<completed_work>
|
||||
- 30.09.: Windows-Test bestanden, Review seit 26.09. + Fixes, v1.8.0.
|
||||
- Dashboard-Skalierung (nie scrollen), eigene Module Keep-Alive, Favoriten enger.
|
||||
- Sicherheit: Rolle/Aktiv-Status je Anfrage aus DB; /login-Weiterleitung.
|
||||
- Willkommensmail + eigene Vorlage (Platzhalter, Vorschau, Testmail, Anmeldehinweise), Spalte Letzte Anmeldung; v1.9.0.
|
||||
</completed_work>
|
||||
|
||||
<remaining_work>
|
||||
- User: live auf 1.9.0 ziehen (df -h / vorher), Willkommensmail in OWA prüfen.
|
||||
</remaining_work>
|
||||
|
||||
<next_action>
|
||||
Nachfragen, ob live gezogen und OWA ok; sonst neuen Auftrag abwarten.
|
||||
</next_action>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"timestamp": "2026-09-30T18:00:00.000Z",
|
||||
"phase": null,
|
||||
"phase_name": "Quick-Auftraege nach Freigabe 1.9.0 (keine GSD-Phase)",
|
||||
"phase_dir": null,
|
||||
"plan": null,
|
||||
"task": 0,
|
||||
"total_tasks": 0,
|
||||
"status": "paused",
|
||||
"completed_tasks": [
|
||||
{"id": 1, "name": "30.09.: Windows-Test (Tray-Update + Erinnerungs-Toast) bestanden; Review aller Aenderungen seit 26.09. + Fixes; Freigabe 1.8.0 (af78157)", "status": "done"},
|
||||
{"id": 2, "name": "Dashboard 1:1-Skalierung (__canvas, passt Inhalt ein = nie scrollen), eigene Module Keep-Alive (max 5), Favoriten-Kachelansicht enger + lange Namen klein/zweizeilig", "status": "done"},
|
||||
{"id": 3, "name": "Sicherheit: JwtStrategy liest Rolle/isActive/mustChangePassword je Anfrage aus DB; /login leitet Angemeldete aufs Dashboard", "status": "done"},
|
||||
{"id": 4, "name": "Willkommensmail (Briefsymbol Benutzerliste, jederzeit an jeden) + Spalte Letzte Anmeldung + eigene Vorlage je Mandant (Admin -> Willkommensmail, Platzhalter, Vorschau, Testmail, Anmeldehinweise editierbar); Freigabe 1.9.0 (a257bc3)", "status": "done"}
|
||||
],
|
||||
"remaining_tasks": [],
|
||||
"blockers": [],
|
||||
"async_jobs": [],
|
||||
"human_actions_pending": [
|
||||
{"action": "Live-Server auf v1.9.0 ziehen (vorher df -h /, ggf. docker image prune -f, nie -a)", "context": "CI 11 Laeufe gruen, Release Tessera 1.9.0 mit Setup.exe + AppImage", "blocking": false},
|
||||
{"action": "Willkommensmail in OWA (owa.ctl.de) pruefen: dunkler Kopf ohne weisse Luecke", "context": "OWA zeigt CID-Bilder nicht, Outlook-Programm schon", "blocking": false}
|
||||
],
|
||||
"decisions": [
|
||||
{"decision": "Dashboard: alles mitskalieren inkl. Schrift, nie scrollen; Leinwand = Flaeche beim ersten Oeffnen je Reiter", "rationale": "AskUserQuestion 30.09.", "phase": "quick"},
|
||||
{"decision": "Keine naechtliche Docker-Aufraeumung auf alpha", "rationale": "User 30.09.: passt so", "phase": "quick"},
|
||||
{"decision": "Willkommensmail jederzeit an jeden Benutzer; Link Passwort festlegen 7 Tage", "rationale": "User 30.09.", "phase": "quick"},
|
||||
{"decision": "PMG ohne API-Token (Proxmox kennt keine), eigener Auditor-Benutzer; Anleitung im Admin-Handbuch", "rationale": "Proxmox Bugzilla 5849 offen", "phase": "quick"}
|
||||
],
|
||||
"uncommitted_files": [],
|
||||
"next_action": "Nichts offen von Claudes Seite. Beim Start: fragen, ob live auf 1.9.0 gezogen ist und ob die Willkommensmail in OWA passt; sonst neuen Auftrag abwarten.",
|
||||
"context_notes": "main = live = v1.9.0 (a257bc3). alpha lief zuletzt auf 714f731 (= Inhalt 1.9.0). Lokaler Stack aus 714f731 gebaut. Test-Postfach MailHog nur bei Bedarf: docker run -d --rm --name mailhog --network tessera-ctl_backend-net --network-alias mailhog -p 127.0.0.1:8025:8025 mailhog/mailhog. Diagnose auf alpha ohne Passwort: JWT im api-Container mit crypto + process.env.JWT_SECRET signieren (nur lesend, kurzlebig)."
|
||||
}
|
||||
+3
-2
@@ -6,7 +6,7 @@ current_phase_name: desktop-client-fertigstellen
|
||||
status: verified
|
||||
stopped_at: "22.09.2026: 1.3.0 freigegeben; danach quick-260922-hk4 — Bilderrahmen-Bilder liegen jetzt im Dateibereich (user-files) statt in der Datenbank, Umzug laeuft automatisch beim Start, Selbstheilung aus der alten data-Spalte eingebaut; im Browser nachgewiesen. NAECHSTER SCHRITT, vom Nutzer noch nicht bestaetigt: (1) einmaliges Aufraeumen, damit ein Modul seine Dashboard-Kachel selbst mitbringt (heute sieben Hartkodierungen je Kachel; Katalog zeigt auch Kacheln gesperrter Module; gesperrte Kachel bleibt leer statt zu erklaeren) — das Geruest WIDGET_MODULE_MAP existiert und ist leer; (2) danach das Proxmox-Modul (PVE/PBS/PMG) und seine Kachel. Offen beim Nutzer: Live-Server auf 1.3.0 ziehen, neuen Client per Browser installieren."
|
||||
last_updated: "2026-09-23T15:30:00.000Z"
|
||||
last_activity: 2026-09-30
|
||||
last_activity: 2026-10-01
|
||||
last_activity_desc: Quick 260928-ujj — Design Mosaik uebernommen, Hintergrund pro Benutzer in der DB; Freigabe 1.5.0
|
||||
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
|
||||
progress:
|
||||
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
||||
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
||||
Plan: 6 of 6
|
||||
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
|
||||
Last activity: 2026-09-30 - Windows-Test (Tray-Update + Erinnerungs-Toast) bestanden; Review aller Aenderungen seit 26.09. mit 4 Fix-Commits (be1e003, c2e4467, 0710829, 12214a9)
|
||||
Last activity: 2026-09-30 - v1.9.0 freigegeben (Willkommensmail + Vorlage, Rollen je Anfrage aus DB, Dashboard-Skalierung, Keep-Alive eigene Module); pausiert mit HANDOFF
|
||||
|
||||
Progress: [██████████] 99%
|
||||
|
||||
@@ -483,6 +483,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 260929-dzu | **Eigene Module fuer jeden Benutzer (persoenlich).** `CustomModule.ownerUserId` (null = gemeinsam), RLS-Muster SearchProvider, Einstellungen > Eigene Module (nur eigene), Verwaltung nur gemeinsame; Browser: Sichtbarkeit/Rechte wie verlangt. Nebenbei ohne eigenen Quick: Zentrierung entfernt (bc4c011), Desktop neue Fenster -> System-Browser (76a9234, Windows-VM bestaetigt), Single-Instance auf VM bestaetigt. | 2026-09-29 | c703d87,ee97b4e,8f41bd2 | [260929-dzu-eigene-module-fuer-jeden-benutzer-persoe](./quick/260929-dzu-eigene-module-fuer-jeden-benutzer-persoe/) |
|
||||
| 260929-if2 | **Erinnerungen-Widget (Reminder).** Modell `Reminder` + RLS, API /reminders (anlegen/listen/bearbeiten/loeschen/erledigt/snooze, 409/404-Regeln), E-Mail-Scheduler alle 30 s mit Claim-once + max. 3 Versuche, globaler ReminderNotifier (Browser-Notification, Desktop via Tauri-Notification mit Laufzeit-Capability nur fuer die Server-Origin, Pattern escaped + vorab geprueft). Verifier human_needed (Windows-Toast offen); Browser dunkel bestanden inkl. echter Mail ueber MailHog. api 1570, web 1069, cargo 57. Nebenbei: eigene Module ohne Kopfzeile (cd1f8f6), Update-Klick prueft frisch (41d00a3). | 2026-09-29 | 325c5dd,709b41a,6879c75 | [260929-if2-reminder-widget-mit-benachrichtigung](./quick/260929-if2-reminder-widget-mit-benachrichtigung/) |
|
||||
| 260929-lh3 | **Favoriten: eigene Symbol-Adresse wirkt.** Neue iconUrl ersetzt Upload + bumpt iconVersion; iconUrl wird auch gespeichert, wenn nur der Browser sie laden kann (kein 422 mehr, nur Formpruefung); Kachel: Proxy -> iconUrl direkt -> origin/favicon -> Buchstabe; Discovery liest <link rel=icon> auch aus Nicht-2xx-Seiten (docuvita 400). | 2026-09-29 | 7188c5b,b15c746,0e72ad4 | [260929-lh3-favoriten-eigenes-symbol-wirkt-nicht](./quick/260929-lh3-favoriten-eigenes-symbol-wirkt-nicht/) |
|
||||
| 261001-cxo | Desktop-Client: Links mit target=_blank (Favoriten) oeffnen jetzt im System-Browser (DesktopExternalLinks -> window.open) | 2026-10-01 | 61a971c | [261001-cxo](./quick/261001-cxo-desktop-client-links-mit-target-blank-oe/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
---
|
||||
quick_id: 261001-cxo
|
||||
description: "Desktop-Client: Links mit target=_blank oeffnen"
|
||||
date: 2026-10-01
|
||||
---
|
||||
|
||||
# Desktop-Client: Links mit target=_blank oeffnen
|
||||
|
||||
**Befund (VM 8233, Client 1.9.0 gegen alpha):** Klick auf Favorit (`<a target="_blank">`) tut nichts; Such-Widget (`window.open`) oeffnet Edge ueber `on_new_window` (lib.rs). Der Rust-Weg funktioniert also, nur der Link-Klick erreicht ihn nicht.
|
||||
|
||||
## Task 1 — DesktopExternalLinks
|
||||
- `apps/web/src/components/desktop/desktop-external-links.tsx`: im Desktop-Client (Cookie `tessera_desktop`) Links-/Mittelklick auf `a[href][target=_blank]` mit http/https per `window.open(href,'_blank','noopener,noreferrer')` oeffnen, `preventDefault`. Listener auf `window` (Bubble, nach React) -> von der Seite verhinderte Klicks (Favoriten im Bearbeiten-Modus) bleiben verhindert.
|
||||
- In `apps/web/src/app/layout.tsx` neben `DesktopContextMenuGuard` einhaengen.
|
||||
- Test `desktop-external-links.test.tsx`.
|
||||
- CHANGELOG „Unveröffentlicht → Behoben“.
|
||||
|
||||
**Verify:** vitest gruen, tsc, biome; nach alpha-Pull auf VM 8233: Favorit oeffnet Edge.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
---
|
||||
quick_id: 261001-cxo
|
||||
status: complete
|
||||
date: 2026-10-01
|
||||
commit: 61a971c
|
||||
---
|
||||
|
||||
# Summary: Desktop-Client – Links mit target=_blank
|
||||
|
||||
- Nachgestellt auf VM 8233 (Client 1.9.0, alpha): Favorit-Klick ohne Wirkung, Such-Widget (`window.open`) oeffnet Edge.
|
||||
- Neu `DesktopExternalLinks` (apps/web/src/components/desktop/desktop-external-links.tsx), in `app/layout.tsx` eingehaengt: im Client Links-/Mittelklick auf `a[target=_blank]` mit http/https -> `window.open(href,'_blank','noopener,noreferrer')`; verhinderte Klicks bleiben verhindert.
|
||||
- 4 Tests (desktop-external-links.test.tsx), tsc + biome sauber. CHANGELOG „Unveröffentlicht → Behoben“.
|
||||
- Reine Web-Aenderung: kein neuer Client noetig, wirkt nach Pull des web-Images.
|
||||
- Offen: Nachweis auf VM nach alpha-Pull (User).
|
||||
|
||||
## Nachtrag (gleicher Tag): erste Fassung wirkte nicht
|
||||
- Nach alpha-Pull weiter ohne Wirkung. Diagnose per temporaerem Klick-Protokoll (lokaler Stack, VM-Client per portproxy auf localhost:3000): `preventDefault` kam aus `<anonymous>:1:442` = Link-Skript von tauri-plugin-opener (init-iife.js, Listener auf `window`): faengt `target=_blank`-Klicks ab und ruft `plugin:opener|open_url` – von der Server-Seite nicht freigegeben, Klick verpufft. Unser Listener auf `window` lief danach und sah den Klick als verhindert.
|
||||
- Fix: Listener auf `document` (Bubble) – nach React (Wurzel document), vor dem Opener-Skript. Auf VM nachgewiesen: Favorit oeffnet Edge, im Bearbeiten-Modus nichts (React-onClick verhindert).
|
||||
- Commit siehe git log; Test „kommt dem Link-Skript des Clients auf window zuvor“.
|
||||
@@ -4,6 +4,10 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
||||
|
||||
## Unveröffentlicht
|
||||
|
||||
### Behoben
|
||||
|
||||
- Desktop-App: Favoriten und andere Links, die sich in einem neuen Fenster öffnen (etwa „In neuem Tab öffnen“ oder Quellen im Ausschreibungs-Radar), öffnen sich jetzt in Ihrem normalen Browser. Bisher passierte beim Klick in der Desktop-App nichts.
|
||||
|
||||
## 1.9.0 – 2026-09-30
|
||||
|
||||
### Neu
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getLocale, getMessages } from 'next-intl/server';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { DesktopContextMenuGuard } from '@/components/desktop/desktop-context-menu-guard';
|
||||
import { DesktopExternalLinks } from '@/components/desktop/desktop-external-links';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -10,11 +11,7 @@ export const metadata: Metadata = {
|
||||
description: 'Modulare Workflow-Plattform',
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const locale = await getLocale();
|
||||
const messages = await getMessages();
|
||||
|
||||
@@ -29,6 +26,7 @@ export default async function RootLayout({
|
||||
>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<DesktopContextMenuGuard />
|
||||
<DesktopExternalLinks />
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { act, cleanup, render } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DESKTOP_COOKIE_NAME } from '@/lib/desktop-client';
|
||||
import { DesktopExternalLinks } from './desktop-external-links';
|
||||
|
||||
/**
|
||||
* desktop-external-links.test (quick-261001-cxo) — im Desktop-Client gehen
|
||||
* Klicks auf Links mit `target="_blank"` ueber `window.open`; ohne Cookie,
|
||||
* bei verhinderten Klicks, ohne `_blank` und bei Nicht-http(s) bleibt alles
|
||||
* unberuehrt. `fire` gibt den `dispatchEvent`-Rueckgabewert zurueck:
|
||||
* `false` bedeutet, `preventDefault()` wurde aufgerufen.
|
||||
*/
|
||||
function setCookie() {
|
||||
document.cookie = `${DESKTOP_COOKIE_NAME}=1; path=/`;
|
||||
}
|
||||
|
||||
function clearCookie() {
|
||||
document.cookie = `${DESKTOP_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
||||
}
|
||||
|
||||
function link(href: string, target?: string) {
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
if (target) a.target = target;
|
||||
const span = document.createElement('span');
|
||||
a.appendChild(span);
|
||||
document.body.appendChild(a);
|
||||
return span;
|
||||
}
|
||||
|
||||
function fire(el: Element, type: 'click' | 'auxclick' = 'click', button = 0) {
|
||||
return el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, button }));
|
||||
}
|
||||
|
||||
let openSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
clearCookie();
|
||||
openSpy.mockRestore();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('DesktopExternalLinks', () => {
|
||||
it('mit Cookie: Klick auf _blank-Link oeffnet per window.open', async () => {
|
||||
setCookie();
|
||||
render(<DesktopExternalLinks />);
|
||||
await act(async () => {});
|
||||
|
||||
const inner = link('https://chatgpt.com/', '_blank');
|
||||
expect(fire(inner)).toBe(false);
|
||||
expect(openSpy).toHaveBeenCalledWith('https://chatgpt.com/', '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
|
||||
it('mit Cookie: kommt dem Link-Skript des Clients auf window zuvor', async () => {
|
||||
setCookie();
|
||||
render(<DesktopExternalLinks />);
|
||||
await act(async () => {});
|
||||
|
||||
// Nachbau von tauri-plugin-opener (init-iife.js): bricht ab, wenn der
|
||||
// Klick schon verhindert ist, sonst preventDefault + IPC-Aufruf.
|
||||
const openerSaw: boolean[] = [];
|
||||
const opener = (e: MouseEvent) => openerSaw.push(e.defaultPrevented);
|
||||
window.addEventListener('click', opener);
|
||||
try {
|
||||
fire(link('https://chatgpt.com/', '_blank'));
|
||||
} finally {
|
||||
window.removeEventListener('click', opener);
|
||||
}
|
||||
expect(openSpy).toHaveBeenCalledTimes(1);
|
||||
expect(openerSaw).toEqual([true]);
|
||||
});
|
||||
|
||||
it('mit Cookie: Mittelklick auf _blank-Link oeffnet ebenfalls', async () => {
|
||||
setCookie();
|
||||
render(<DesktopExternalLinks />);
|
||||
await act(async () => {});
|
||||
|
||||
const inner = link('http://intranet.local/', '_blank');
|
||||
expect(fire(inner, 'auxclick', 1)).toBe(false);
|
||||
expect(openSpy).toHaveBeenCalledWith('http://intranet.local/', '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
|
||||
it('mit Cookie: verhinderter Klick, Link ohne _blank und mailto bleiben unberuehrt', async () => {
|
||||
setCookie();
|
||||
render(<DesktopExternalLinks />);
|
||||
await act(async () => {});
|
||||
|
||||
const prevented = link('https://chatgpt.com/', '_blank');
|
||||
prevented.addEventListener('click', (e) => e.preventDefault());
|
||||
fire(prevented);
|
||||
|
||||
expect(fire(link('https://example.com/'))).toBe(true);
|
||||
expect(fire(link('mailto:a@example.com', '_blank'))).toBe(true);
|
||||
expect(fire(link('https://example.com/', '_blank'), 'click', 2)).toBe(true);
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ohne Cookie: keine Umleitung', async () => {
|
||||
render(<DesktopExternalLinks />);
|
||||
await act(async () => {});
|
||||
|
||||
expect(fire(link('https://chatgpt.com/', '_blank'))).toBe(true);
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useIsDesktopClient } from '@/lib/desktop-client';
|
||||
|
||||
/**
|
||||
* DesktopExternalLinks (quick-261001-cxo) — Links mit `target="_blank"`
|
||||
* (Favoriten, „In neuem Tab öffnen“ bei XFrame/eigenen Modulen, Quellen im
|
||||
* Ausschreibungs-Radar) taten im Desktop-Client beim Klick nichts, waehrend
|
||||
* `window.open` (Such-Widget) ueber `on_new_window` im System-Browser landet.
|
||||
*
|
||||
* Ursache (VM 8233, 01.10.2026, gemessen): tauri-plugin-opener schleust ein
|
||||
* Skript ein, das auf `window` solche Klicks abfaengt, `preventDefault()`
|
||||
* ruft und `plugin:opener|open_url` aufruft -- von der Server-Seite aus ist
|
||||
* dieser Aufruf nicht freigegeben, der Klick verpufft. Dieser Helfer kommt
|
||||
* ihm zuvor und oeffnet per `window.open`; das Opener-Skript sieht den Klick
|
||||
* dann als verhindert und tut nichts.
|
||||
*
|
||||
* Lauscht deshalb auf `document` in der Bubble-Phase: NACH Reacts Handlern
|
||||
* (Wurzel `document`, frueher registriert) -- ein Klick, den die Seite selbst
|
||||
* verhindert (Favoriten im Bearbeiten-Modus), bleibt verhindert --, aber VOR
|
||||
* dem Opener-Skript auf `window`. Nur http/https.
|
||||
*/
|
||||
function externalTarget(event: MouseEvent): HTMLAnchorElement | null {
|
||||
if (event.defaultPrevented) return null;
|
||||
// Linksklick und Mittelklick oeffnen beide ein neues Fenster.
|
||||
if (event.button !== 0 && event.button !== 1) return null;
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return null;
|
||||
const anchor = target.closest('a[href]');
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return null;
|
||||
if (anchor.target !== '_blank') return null;
|
||||
if (anchor.protocol !== 'http:' && anchor.protocol !== 'https:') return null;
|
||||
return anchor;
|
||||
}
|
||||
|
||||
export function DesktopExternalLinks() {
|
||||
const isDesktop = useIsDesktopClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop) return;
|
||||
|
||||
const handler = (event: MouseEvent) => {
|
||||
if (event.type === 'auxclick' && event.button !== 1) return;
|
||||
if (event.type === 'click' && event.button !== 0) return;
|
||||
const anchor = externalTarget(event);
|
||||
if (!anchor) return;
|
||||
event.preventDefault();
|
||||
window.open(anchor.href, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
document.addEventListener('click', handler);
|
||||
document.addEventListener('auxclick', handler);
|
||||
return () => {
|
||||
document.removeEventListener('click', handler);
|
||||
document.removeEventListener('auxclick', handler);
|
||||
};
|
||||
}, [isDesktop]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user