feat(quick-260922-ge2): XFrame-Widget - Ausschnitt der Seite waehlen und einpassen, Zoom fuer die ganze Seite, Nur anzeigen
- Resolver: crop (x/y/w/h, geklemmt auf 1280er-Seite, x verschoben statt abgewiesen), zoom (50..150, groesste Stufe <= n), readOnly (nur echtes true) - xframe-crop.ts: reine Geometrie - computeCropLayout (contain + zentriert, scale 0 bis gemessen), applyCropDrag (Verschieben, vier Ecken, Gegenecke bleibt) - Kachel: Clip + verschobener, skalierter <iframe> bei fester Layoutbreite 1280, ResizeObserver am Koerper; Zoom-Zweig mit Prozentmassen; 100 % wie bisher; readOnly-Flaeche nur im Ansichtsmodus - Formular: Checkbox Ausschnitt (ein Aufruf mit crop + readOnly), Vorschau 1280 px breit mit derselben Sandbox und pointer-events none, Rahmen mit vier Griffen (Pointer-Events, Capture-Waechter fuer jsdom), Zahlenfelder, Zoom-Auswahl nur ohne Ausschnitt, Nur anzeigen, dauerhafter Hinweis - Rahmen als <fieldset> statt div role=group (Biome useSemanticElements, kein biome-ignore) - Test-Helfer stubResizeObserver, 13 neue Schluessel de/en, Ausschnitt auf der Umlaut-Allowlist; Web-Tests 604 -> 640 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,12 +41,50 @@ export const XFRAME_SANDBOX =
|
||||
// ganzen Dashboard (echter URL-Parser, Protokoll kleingeschrieben verglichen).
|
||||
export { isHttpsUrl };
|
||||
|
||||
/** Virtuelle Layoutbreite der eingebetteten Seite in Pixeln — Konstante, keine UI. */
|
||||
export const XFRAME_PAGE_WIDTH = 1280;
|
||||
export const XFRAME_CROP_MIN_W = 100;
|
||||
export const XFRAME_CROP_MIN_H = 60;
|
||||
/** Speichergrenze fuer getippte Werte; die Vorschau zeigt 3000 (xframe-crop.ts). */
|
||||
export const XFRAME_CROP_MAX_H = 4000;
|
||||
/** Ausschnitt in Seitenpixeln bei XFRAME_PAGE_WIDTH. */
|
||||
export interface XframeCrop {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
export const XFRAME_CROP_DEFAULT: XframeCrop = { x: 0, y: 0, w: XFRAME_PAGE_WIDTH, h: 720 };
|
||||
/** Vergroesserung der ganzen Seite in Prozent (nur ohne Ausschnitt). */
|
||||
export const XFRAME_ZOOM_OPTIONS: number[] = [50, 60, 75, 90, 100, 125, 150];
|
||||
export const XFRAME_ZOOM_DEFAULT = 100;
|
||||
|
||||
export interface XframeConfig {
|
||||
/** null = keine gueltige https-Adresse -> Leerzustand. */
|
||||
url: string | null;
|
||||
title: string;
|
||||
/** 0 = nie, sonst eine Stufe aus XFRAME_RELOAD_OPTIONS. */
|
||||
reloadSeconds: number;
|
||||
/** null = ganze Seite; sonst geklemmter Ausschnitt (quick-260922-ge2). */
|
||||
crop: XframeCrop | null;
|
||||
/** Stufe aus XFRAME_ZOOM_OPTIONS, wirkt nur bei crop === null. */
|
||||
zoom: number;
|
||||
/** Transparente Flaeche ueber dem Rahmen im Ansichtsmodus. */
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EINE Klemmregel fuer Resolver, Zahlenfelder und Ziehen: runden, Breite
|
||||
* [100, 1280], Hoehe [60, 4000], Position >= 0, dann x so verschieben, dass
|
||||
* der Ausschnitt in der Seite bleibt (nie abweisen, T-GE2-05).
|
||||
*/
|
||||
export function clampXframeCrop(raw: XframeCrop): XframeCrop {
|
||||
const w = Math.min(XFRAME_PAGE_WIDTH, Math.max(XFRAME_CROP_MIN_W, Math.round(raw.w)));
|
||||
const h = Math.min(XFRAME_CROP_MAX_H, Math.max(XFRAME_CROP_MIN_H, Math.round(raw.h)));
|
||||
let x = Math.max(0, Math.round(raw.x));
|
||||
const y = Math.max(0, Math.round(raw.y));
|
||||
if (x + w > XFRAME_PAGE_WIDTH) x = XFRAME_PAGE_WIDTH - w;
|
||||
return { x, y, w, h };
|
||||
}
|
||||
|
||||
function resolveUrl(raw: unknown): string | null {
|
||||
@@ -75,10 +113,44 @@ function resolveReload(raw: unknown): number {
|
||||
return best;
|
||||
}
|
||||
|
||||
function finiteNumber(raw: unknown): raw is number {
|
||||
return typeof raw === 'number' && Number.isFinite(raw);
|
||||
}
|
||||
|
||||
/** Typwaechter statt Cast: ein Nicht-null-Objekt darf als Schluessel-Wert-Menge gelesen werden. */
|
||||
function isRecord(raw: unknown): raw is Record<string, unknown> {
|
||||
return typeof raw === 'object' && raw !== null;
|
||||
}
|
||||
|
||||
/** Nur ein Nicht-null-Objekt mit vier endlichen Zahlen x, y, w, h -> geklemmt; alles andere -> null. */
|
||||
function resolveCrop(raw: unknown): XframeCrop | null {
|
||||
if (!isRecord(raw)) return null;
|
||||
const { x, y, w, h } = raw;
|
||||
if (!finiteNumber(x) || !finiteNumber(y) || !finiteNumber(w) || !finiteNumber(h)) return null;
|
||||
return clampXframeCrop({ x, y, w, h });
|
||||
}
|
||||
|
||||
/** Groesste Zoom-Stufe <= n (Muster resolveReload); unter 50 -> 50; nicht endlich -> 100. */
|
||||
function resolveZoom(raw: unknown): number {
|
||||
if (!finiteNumber(raw)) return XFRAME_ZOOM_DEFAULT;
|
||||
let best = XFRAME_ZOOM_OPTIONS[0];
|
||||
for (const option of XFRAME_ZOOM_OPTIONS) {
|
||||
if (option <= raw && option > best) best = option;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function resolveReadOnly(raw: unknown): boolean {
|
||||
return raw === true;
|
||||
}
|
||||
|
||||
export function resolveXframeConfig(config: Record<string, unknown>): XframeConfig {
|
||||
return {
|
||||
url: resolveUrl(config.url),
|
||||
title: resolveTitle(config.title),
|
||||
reloadSeconds: resolveReload(config.reloadSeconds),
|
||||
crop: resolveCrop(config.crop),
|
||||
zoom: resolveZoom(config.zoom),
|
||||
readOnly: resolveReadOnly(config.readOnly),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user