fix: v4.6.0 - cross-day drag: releasePointerCapture + bounding-rect day detection (no elementFromPoint)
Some checks failed
Build & Push Docker / build (push) Has been cancelled

This commit is contained in:
2026-02-20 19:16:32 +01:00
parent ad41f338f8
commit d38d7588f3
3 changed files with 68 additions and 41 deletions

View File

@@ -1,5 +1,5 @@
"""Application configuration."""
VERSION = "4.5.0"
VERSION = "4.6.0"
MAX_FILE_SIZE_MB = 10
DEFAULT_COLOR = "#ffffff"

View File

@@ -13,7 +13,7 @@
<header class="header">
<div class="header-left">
<h1 class="header-title">Scénář Creator</h1>
<span class="header-version">v4.5</span>
<span class="header-version">v4.6</span>
</div>
<div class="header-actions">
<label class="btn btn-secondary btn-sm" id="importJsonBtn">

View File

@@ -300,16 +300,30 @@ const Canvas = {
return minutes * this.pxPerMinute;
},
// Native pointer drag — creates a ghost on document.body so clipping never happens
// Native pointer drag — ghost on document.body (no overflow/clipping issues)
// Day detection uses pre-captured bounding rects, NOT elementFromPoint (unreliable with ghost)
_startPointerDrag(e, el, block) {
const startX = e.clientX;
const startY = e.clientY;
const elRect = el.getBoundingClientRect();
const startMin = this.parseTime(block.start);
const duration = this.parseTime(block.end) - startMin;
const snapGrid = this._minutesPx(this.GRID_MINUTES);
const startX = e.clientX;
const startY = e.clientY;
const elRect = el.getBoundingClientRect();
const startMin = this.parseTime(block.start);
const duration = this.parseTime(block.end) - startMin;
const snapGrid = this._minutesPx(this.GRID_MINUTES);
// Create floating ghost
// ── Capture day row positions BEFORE any DOM change ──────────
const dayTimelines = Array.from(document.querySelectorAll('.day-timeline'));
const dayRows = dayTimelines.map(t => {
const r = t.getBoundingClientRect();
return { date: t.dataset.date, el: t, top: r.top, bottom: r.bottom };
});
// Release implicit pointer capture so pointermove fires freely on document
try { el.releasePointerCapture(e.pointerId); } catch (_) {}
// Prevent text selection during drag
document.body.style.userSelect = 'none';
// ── Ghost element ─────────────────────────────────────────────
const ghost = document.createElement('div');
ghost.className = 'block-el drag-ghost';
ghost.innerHTML = el.innerHTML;
@@ -325,52 +339,65 @@ const Canvas = {
opacity: '0.88',
pointerEvents: 'none',
cursor: 'grabbing',
boxShadow: '0 6px 20px rgba(0,0,0,0.28)',
boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
borderRadius: '4px',
transition: 'none',
margin: '0',
});
document.body.appendChild(ghost);
el.style.opacity = '0.25'; // dim original, don't hide (keeps layout)
el.style.opacity = '0.2';
const clearHighlights = () =>
document.querySelectorAll('.day-timeline.drag-target')
.forEach(r => r.classList.remove('drag-target'));
const onMove = (ev) => {
const dx = ev.clientX - startX;
const dy = ev.clientY - startY;
ghost.style.left = (elRect.left + dx) + 'px';
ghost.style.top = (elRect.top + dy) + 'px';
// Highlight target row — elementFromPoint sees through ghost (pointer-events:none)
clearHighlights();
const under = document.elementFromPoint(ev.clientX, ev.clientY);
under?.closest('.day-timeline')?.classList.add('drag-target');
// ── Helpers ───────────────────────────────────────────────────
const findRow = (clientY) => {
for (const d of dayRows) {
if (clientY >= d.top && clientY <= d.bottom) return d;
}
// Clamp to nearest row when pointer is between rows or outside canvas
if (!dayRows.length) return null;
if (clientY < dayRows[0].top) return dayRows[0];
return dayRows[dayRows.length - 1];
};
const clearHighlights = () =>
dayTimelines.forEach(r => r.classList.remove('drag-target'));
// ── Drag move ─────────────────────────────────────────────────
const onMove = (ev) => {
ghost.style.left = (elRect.left + ev.clientX - startX) + 'px';
ghost.style.top = (elRect.top + ev.clientY - startY) + 'px';
clearHighlights();
const row = findRow(ev.clientY);
if (row) row.el.classList.add('drag-target');
};
// ── Drag end ──────────────────────────────────────────────────
const onUp = (ev) => {
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
const dx = ev.clientX - startX;
// Snap X to 15-min grid
const snappedDx = Math.round(dx / snapGrid) * snapGrid;
const deltaMin = snappedDx / this.pxPerMinute;
const newStart = this.snapMinutes(startMin + deltaMin);
const clamped = Math.max(this._startMin, Math.min(this._endMin - duration, newStart));
block.start = this.formatTime(clamped);
block.end = this.formatTime(clamped + duration);
// Determine target day
const under = document.elementFromPoint(ev.clientX, ev.clientY);
const timeline = under?.closest('.day-timeline');
if (timeline?.dataset.date) block.date = timeline.dataset.date;
ghost.remove();
el.style.opacity = '';
clearHighlights();
document.body.style.userSelect = '';
const dx = ev.clientX - startX;
const dy = ev.clientY - startY;
// Ignore micro-movements (treat as click, let click handler open modal)
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
// ── Update time (X axis) ──────────────────────────────────
const snappedDx = Math.round(dx / snapGrid) * snapGrid;
const deltaMin = snappedDx / this.pxPerMinute;
const newStart = this.snapMinutes(startMin + deltaMin);
const clamped = Math.max(this._startMin, Math.min(this._endMin - duration, newStart));
block.start = this.formatTime(clamped);
block.end = this.formatTime(clamped + duration);
// ── Update day (Y axis) — bounding rects, NO elementFromPoint ──
const row = findRow(ev.clientY);
if (row) block.date = row.date;
App.renderCanvas();
};