v0.12.1
This commit is contained in:
+30
-106
@@ -151,6 +151,9 @@ function updateChartFullscreenButton(card) {
|
||||
function closeChartPreview(card) {
|
||||
if (!card) return;
|
||||
card.classList.remove('chart-fullscreen-fallback');
|
||||
card.removeAttribute('role');
|
||||
card.removeAttribute('aria-modal');
|
||||
card.removeAttribute('aria-label');
|
||||
document.body.classList.remove('chart-fullscreen-open');
|
||||
updateChartFullscreenButton(card);
|
||||
const canvas = card.querySelector('canvas[id]');
|
||||
@@ -158,6 +161,7 @@ function closeChartPreview(card) {
|
||||
}
|
||||
|
||||
function toggleChartFullscreen(id) {
|
||||
if (!window.matchMedia('(min-width: 761px)').matches) return;
|
||||
const canvas = document.getElementById(id);
|
||||
const card = canvas?.closest('.history-chart-card');
|
||||
if (!card) return;
|
||||
@@ -172,12 +176,10 @@ function toggleChartFullscreen(id) {
|
||||
}
|
||||
|
||||
card.classList.add('chart-fullscreen-fallback');
|
||||
card.setAttribute('role', 'dialog');
|
||||
card.setAttribute('aria-modal', 'true');
|
||||
card.setAttribute('aria-label', card.querySelector('h3')?.textContent || tr('history.fullscreen'));
|
||||
document.body.classList.add('chart-fullscreen-open');
|
||||
if (window.matchMedia('(max-width: 760px)').matches) {
|
||||
app.chartZooms[id] = 1;
|
||||
canvas.dataset.chartZoom = '1';
|
||||
if (canvas.parentElement) canvas.parentElement.scrollLeft = 0;
|
||||
}
|
||||
updateChartFullscreenButton(card);
|
||||
requestAnimationFrame(() => redrawHistoryChart(id));
|
||||
}
|
||||
@@ -186,15 +188,12 @@ function prepareCanvas(canvas, height) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrap = canvas.parentElement;
|
||||
const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720);
|
||||
const card = canvas.closest('.history-chart-card');
|
||||
const fullscreen = chartCardIsFullscreen(card);
|
||||
const mobileFullscreen = fullscreen && window.matchMedia('(max-width: 760px)').matches;
|
||||
const fullscreen = chartCardIsFullscreen(canvas.closest('.history-chart-card'));
|
||||
const wrapHeight = fullscreen ? Math.floor(wrap?.clientHeight || 0) : 0;
|
||||
const naturalMobileHeight = Math.round(clamp(wrapWidth * .72, 260, 360));
|
||||
const actualHeight = mobileFullscreen ? Math.min(wrapHeight || naturalMobileHeight, naturalMobileHeight) : (wrapHeight || height);
|
||||
const actualHeight = wrapHeight || height;
|
||||
const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, MAX_CHART_ZOOM);
|
||||
// Normal charts may scroll horizontally. Fullscreen 100% always fits the viewport width.
|
||||
const baseWidth = fullscreen ? Math.max(1, wrapWidth) : Math.max(720, wrapWidth);
|
||||
// Keep the original chart geometry on narrow screens and let the wrapper scroll horizontally.
|
||||
const baseWidth = Math.max(720, wrapWidth);
|
||||
const width = Math.max(1, Math.floor(baseWidth * zoom));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr; canvas.height = actualHeight * dpr;
|
||||
@@ -262,10 +261,7 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
.filter(point => Number.isFinite(point.ts) && Number.isFinite(point.value))
|
||||
.sort((a, b) => a.ts - b.ts)
|
||||
}));
|
||||
const touches = new Map();
|
||||
let drag = null;
|
||||
let pinch = null;
|
||||
let touchPan = null;
|
||||
|
||||
const hide = () => { tooltip.hidden = true; line.hidden = true; };
|
||||
const hideSelection = () => { if (selection) selection.hidden = true; };
|
||||
@@ -323,114 +319,42 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
};
|
||||
|
||||
canvas.onpointerdown = event => {
|
||||
if (event.pointerType === 'mouse') {
|
||||
if (event.button !== 0) return;
|
||||
const startX = canvasX(event.clientX);
|
||||
drag = { pointerId: event.pointerId, startX, lastX: startX, moved: false };
|
||||
hide(); hideSelection();
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch') {
|
||||
touches.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
if (touches.size === 1) {
|
||||
touchPan = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, scrollLeft: wrap.scrollLeft, moved: false };
|
||||
showFromClientX(event.clientX);
|
||||
}
|
||||
if (touches.size === 2) {
|
||||
touchPan = null;
|
||||
const points = [...touches.values()];
|
||||
const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y);
|
||||
const midX = (points[0].x + points[1].x) / 2;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrapRect = wrap.getBoundingClientRect();
|
||||
pinch = {
|
||||
startDistance: Math.max(1, distance),
|
||||
startZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
baseWidth: (rect.width || width) / clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
anchorRatio: clamp((midX - rect.left) / Math.max(1, rect.width), 0, 1),
|
||||
viewportX: clamp(midX - wrapRect.left, 0, wrap.clientWidth),
|
||||
previewZoom: clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM),
|
||||
};
|
||||
hide();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
if (event.pointerType !== 'mouse' || event.button !== 0) return;
|
||||
const startX = canvasX(event.clientX);
|
||||
drag = { pointerId: event.pointerId, startX, lastX: startX, moved: false };
|
||||
hide(); hideSelection();
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
canvas.onpointermove = event => {
|
||||
if (event.pointerType === 'touch') return;
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) {
|
||||
updateSelection(canvasX(event.clientX));
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch' && touches.has(event.pointerId)) {
|
||||
touches.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||||
if (pinch && touches.size >= 2) {
|
||||
const points = [...touches.values()].slice(0, 2);
|
||||
const distance = Math.hypot(points[0].x - points[1].x, points[0].y - points[1].y);
|
||||
pinch.previewZoom = clamp(pinch.startZoom * (distance / pinch.startDistance), 1, MAX_CHART_ZOOM);
|
||||
const previewWidth = pinch.baseWidth * pinch.previewZoom;
|
||||
canvas.style.width = `${previewWidth}px`;
|
||||
wrap.scrollLeft = Math.max(0, pinch.anchorRatio * previewWidth - pinch.viewportX);
|
||||
const reset = canvas.closest('.history-chart-card')?.querySelector('[data-chart-zoom="reset"]');
|
||||
if (reset) reset.textContent = `${Math.round(pinch.previewZoom * 100)}%`;
|
||||
event.preventDefault();
|
||||
} else if (touchPan?.pointerId === event.pointerId && touches.size === 1) {
|
||||
const dx = event.clientX - touchPan.startX;
|
||||
const dy = event.clientY - touchPan.startY;
|
||||
if (Math.abs(dx) > 6 && Math.abs(dx) > Math.abs(dy)) {
|
||||
touchPan.moved = true;
|
||||
hide();
|
||||
wrap.scrollLeft = Math.max(0, touchPan.scrollLeft - dx);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
showFromClientX(event.clientX);
|
||||
};
|
||||
|
||||
const finishPointer = event => {
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) {
|
||||
const currentX = canvasX(event.clientX);
|
||||
const selectedWidth = Math.abs(currentX - drag.startX);
|
||||
const center = (currentX + drag.startX) / 2;
|
||||
const moved = drag.moved && selectedWidth >= 12;
|
||||
drag = null; hideSelection();
|
||||
try { canvas.releasePointerCapture?.(event.pointerId); } catch (_) { }
|
||||
if (moved) {
|
||||
const currentZoom = clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const factor = Math.max(1, wrap.clientWidth / Math.max(1, selectedWidth));
|
||||
setChartZoom(canvas.id, currentZoom * factor, center / Math.max(1, width), wrap.clientWidth / 2);
|
||||
} else showFromClientX(event.clientX);
|
||||
return;
|
||||
}
|
||||
if (event.pointerType === 'touch') {
|
||||
const wasPan = touchPan?.pointerId === event.pointerId ? touchPan : null;
|
||||
touches.delete(event.pointerId);
|
||||
if (pinch && touches.size < 2) {
|
||||
const completed = pinch;
|
||||
pinch = null;
|
||||
setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX);
|
||||
} else if (wasPan && !wasPan.moved) showFromClientX(event.clientX);
|
||||
if (touches.size === 0) touchPan = null;
|
||||
}
|
||||
if (event.pointerType !== 'mouse' || drag?.pointerId !== event.pointerId) return;
|
||||
const currentX = canvasX(event.clientX);
|
||||
const selectedWidth = Math.abs(currentX - drag.startX);
|
||||
const center = (currentX + drag.startX) / 2;
|
||||
const moved = drag.moved && selectedWidth >= 12;
|
||||
drag = null; hideSelection();
|
||||
try { canvas.releasePointerCapture?.(event.pointerId); } catch (_) { }
|
||||
if (moved) {
|
||||
const currentZoom = clamp(Number(app.chartZooms[canvas.id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const factor = Math.max(1, wrap.clientWidth / Math.max(1, selectedWidth));
|
||||
setChartZoom(canvas.id, currentZoom * factor, center / Math.max(1, width), wrap.clientWidth / 2);
|
||||
} else showFromClientX(event.clientX);
|
||||
};
|
||||
|
||||
canvas.onpointerup = finishPointer;
|
||||
canvas.onpointercancel = event => {
|
||||
if (event.pointerType === 'mouse' && drag?.pointerId === event.pointerId) { drag = null; hideSelection(); }
|
||||
if (event.pointerType === 'touch') {
|
||||
touches.delete(event.pointerId);
|
||||
if (touchPan?.pointerId === event.pointerId) touchPan = null;
|
||||
if (pinch && touches.size < 2) {
|
||||
const completed = pinch;
|
||||
pinch = null;
|
||||
setChartZoom(canvas.id, completed.previewZoom, completed.anchorRatio, completed.viewportX);
|
||||
} else hide();
|
||||
}
|
||||
};
|
||||
canvas.onpointerleave = event => { if (event.pointerType !== 'touch' && !drag) hide(); };
|
||||
canvas.onkeydown = event => {
|
||||
|
||||
Reference in New Issue
Block a user