-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
/**
* Overlay layer for viewing Mermaid diagrams enlarged. Supports drag-to-pan,
* +/- zoom buttons, and pinch-zoom on touch devices.
*/
import { showOverlay, hideOverlay } from './overlay-utils.js';
import { addSafeEventListener, getEl } from './dom-utils.js';
const MIN_SCALE = 0.1;
const MAX_SCALE = 10;
const ZOOM_FACTOR = 1.25;
const FIT_PADDING = 20;
let div_overlay = null;
let div_viewport = null;
let div_stage = null;
let scale = 1;
let pan_x = 0;
let pan_y = 0;
let current_size = null;
const active_pointers = new Map();
let pinch_start = null;
/**
* Checks whether the diagram overlay is currently open.
* @returns {boolean} True if the overlay is active.
*/
export function is_diagram_overlay_open() {
return !!div_overlay && div_overlay.classList.contains('active');
}
/**
* Opens the overlay showing an enlarged clone of the given diagram SVG.
* @param {SVGElement} svg - The rendered Mermaid SVG element.
*/
export function open_diagram_overlay(svg) {
if (!svg) return;
div_overlay = getEl('id-diagram-overlay');
div_viewport = getEl('id-diagram-overlay-viewport');
div_stage = getEl('id-diagram-overlay-stage');
if (!div_overlay || !div_viewport || !div_stage) return;
const clone = /** @type {SVGElement} */ (svg.cloneNode(true));
const size = get_diagram_size(clone);
div_stage.innerHTML = '';
div_stage.style.width = size.width + 'px';
div_stage.style.height = size.height + 'px';
div_stage.appendChild(clone);
current_size = size;
showOverlay(div_overlay, 'flex');
fit_to_view(size);
}
/**
* Closes the diagram overlay if it is open.
*/
export function close_diagram_overlay() {
if (!is_diagram_overlay_open()) return;
active_pointers.clear();
pinch_start = null;
hideOverlay(div_overlay);
}
/**
* Opens the overlay showing a message instead of a diagram, used when a
* diagram cannot be rendered.
* @param {string} message - The message to display.
*/
export function open_diagram_overlay_message(message) {
div_overlay = getEl('id-diagram-overlay');
div_viewport = getEl('id-diagram-overlay-viewport');
div_stage = getEl('id-diagram-overlay-stage');
if (!div_overlay || !div_viewport || !div_stage) return;
div_stage.innerHTML = '';
div_stage.style.width = '';
div_stage.style.height = '';
const div = document.createElement('div');
div.className = 'div-diagram-overlay-message';
div.textContent = message;
div_stage.appendChild(div);
current_size = null;
showOverlay(div_overlay, 'flex');
}
/**
* Binds all overlay interaction events. Call once during app initialization.
*/
export function init_diagram_overlay() {
div_overlay = getEl('id-diagram-overlay');
div_viewport = getEl('id-diagram-overlay-viewport');
div_stage = getEl('id-diagram-overlay-stage');
if (!div_overlay || !div_viewport || !div_stage) return;
addSafeEventListener(getEl('btn-diagram-overlay-close'), 'click', close_diagram_overlay);
addSafeEventListener(getEl('btn-diagram-overlay-zoom-in'), 'click', () => zoom_step(1));
addSafeEventListener(getEl('btn-diagram-overlay-zoom-out'), 'click', () => zoom_step(-1));
addSafeEventListener(getEl('btn-diagram-overlay-reset'), 'click', reset_view);
addSafeEventListener(div_viewport, 'pointerdown', on_pointer_down);
addSafeEventListener(div_viewport, 'pointermove', on_pointer_move);
addSafeEventListener(div_viewport, 'pointerup', on_pointer_end);
addSafeEventListener(div_viewport, 'pointercancel', on_pointer_end);
}
/**
* Reads the natural size of a Mermaid SVG from its viewBox, falling back to
* its rendered bounds.
* @param {SVGElement} svg - The SVG element to measure.
* @returns {{width: number, height: number}} Natural size in pixels.
*/
function get_diagram_size(svg) {
const viewBox = svg?.getAttribute('viewBox');
if (viewBox) {
const parts = viewBox.split(/[\s,]+/).map(parseFloat);
if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
return { width: parts[2], height: parts[3] };
}
}
const rect = svg?.getBoundingClientRect();
return {
width: rect?.width || 600,
height: rect?.height || 400
};
}
/**
* Applies the current pan/zoom transform to the stage element.
*/
function apply_transform() {
if (!div_stage) return;
div_stage.style.transform = 'translate(' + pan_x + 'px, ' + pan_y + 'px) scale(' + scale + ')';
}
/**
* Scales and centers the diagram so it fully fits inside the viewport.
* @param {{width: number, height: number}} size - Natural diagram size in pixels.
*/
function fit_to_view(size) {
const rect = div_viewport.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return;
scale = Math.min(
(rect.width - FIT_PADDING * 2) / size.width,
(rect.height - FIT_PADDING * 2) / size.height
);
scale = Math.min(MAX_SCALE, Math.max(scale, MIN_SCALE));
pan_x = (rect.width - size.width * scale) / 2;
pan_y = (rect.height - size.height * scale) / 2;
apply_transform();
}
/**
* Zooms in or out by one step, centered on the viewport.
* @param {number} direction - 1 to zoom in, -1 to zoom out.
*/
function zoom_step(direction) {
const rect = div_viewport.getBoundingClientRect();
zoom_at(rect.width / 2, rect.height / 2, scale * Math.pow(ZOOM_FACTOR, direction));
}
/**
* Restores the initial centered, fit-to-viewport zoom state.
*/
function reset_view() {
if (!current_size) return;
fit_to_view(current_size);
}
/**
* Zooms to a new scale while keeping the given viewport point fixed.
* @param {number} vx - X coordinate relative to the viewport.
* @param {number} vy - Y coordinate relative to the viewport.
* @param {number} next - The target scale.
*/
function zoom_at(vx, vy, next) {
next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, next));
const k = next / scale;
pan_x = vx - (vx - pan_x) * k;
pan_y = vy - (vy - pan_y) * k;
scale = next;
apply_transform();
}
/**
* Starts tracking a pointer for panning or pinch-zooming.
* @param {PointerEvent} e - The pointer event.
*/
function on_pointer_down(e) {
if (!is_diagram_overlay_open()) return;
div_viewport.setPointerCapture(e.pointerId);
active_pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (active_pointers.size === 2) {
pinch_start = {
dist: get_pointer_distance(),
scale: scale,
pan_x: pan_x,
pan_y: pan_y
};
}
}
/**
* Updates panning or pinch-zooming while pointers move.
* @param {PointerEvent} e - The pointer event.
*/
function on_pointer_move(e) {
const prev = active_pointers.get(e.pointerId);
if (!prev || !is_diagram_overlay_open()) return;
active_pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (active_pointers.size >= 2 && pinch_start && pinch_start.dist > 0) {
const rect = div_viewport.getBoundingClientRect();
const mid = get_pointer_midpoint();
const dist = get_pointer_distance();
const next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinch_start.scale * dist / pinch_start.dist));
const k = next / pinch_start.scale;
const mx = mid.x - rect.left;
const my = mid.y - rect.top;
scale = next;
pan_x = mx - (mx - pinch_start.pan_x) * k;
pan_y = my - (my - pinch_start.pan_y) * k;
apply_transform();
} else if (active_pointers.size === 1) {
pan_x += e.clientX - prev.x;
pan_y += e.clientY - prev.y;
apply_transform();
}
}
/**
* Stops tracking a pointer and resets pinch state when fewer than two remain.
* @param {PointerEvent} e - The pointer event.
*/
function on_pointer_end(e) {
active_pointers.delete(e.pointerId);
if (active_pointers.size < 2) {
pinch_start = null;
}
}
/**
* Computes the distance between the two tracked pointers.
* @returns {number} Distance in pixels, or 0 if fewer than two pointers.
*/
function get_pointer_distance() {
const points = [...active_pointers.values()];
if (points.length < 2) return 0;
return Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y);
}
/**
* Computes the midpoint between the two tracked pointers.
* @returns {{x: number, y: number}} Midpoint in client coordinates.
*/
function get_pointer_midpoint() {
const points = [...active_pointers.values()];
return {
x: (points[0].x + points[1].x) / 2,
y: (points[0].y + points[1].y) / 2
};
}
diagram-overlay.js
×
Type: Web, text/x-java
7.93 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
7.93 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File