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.""" """Application configuration."""
VERSION = "4.5.0" VERSION = "4.6.0"
MAX_FILE_SIZE_MB = 10 MAX_FILE_SIZE_MB = 10
DEFAULT_COLOR = "#ffffff" DEFAULT_COLOR = "#ffffff"

View File

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

View File

@@ -300,16 +300,30 @@ const Canvas = {
return minutes * this.pxPerMinute; 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) { _startPointerDrag(e, el, block) {
const startX = e.clientX; const startX = e.clientX;
const startY = e.clientY; const startY = e.clientY;
const elRect = el.getBoundingClientRect(); const elRect = el.getBoundingClientRect();
const startMin = this.parseTime(block.start); const startMin = this.parseTime(block.start);
const duration = this.parseTime(block.end) - startMin; const duration = this.parseTime(block.end) - startMin;
const snapGrid = this._minutesPx(this.GRID_MINUTES); 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'); const ghost = document.createElement('div');
ghost.className = 'block-el drag-ghost'; ghost.className = 'block-el drag-ghost';
ghost.innerHTML = el.innerHTML; ghost.innerHTML = el.innerHTML;
@@ -325,52 +339,65 @@ const Canvas = {
opacity: '0.88', opacity: '0.88',
pointerEvents: 'none', pointerEvents: 'none',
cursor: 'grabbing', cursor: 'grabbing',
boxShadow: '0 6px 20px rgba(0,0,0,0.28)', boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
borderRadius: '4px', borderRadius: '4px',
transition: 'none', transition: 'none',
margin: '0', margin: '0',
}); });
document.body.appendChild(ghost); document.body.appendChild(ghost);
el.style.opacity = '0.25'; // dim original, don't hide (keeps layout) el.style.opacity = '0.2';
const clearHighlights = () => // ── Helpers ───────────────────────────────────────────────────
document.querySelectorAll('.day-timeline.drag-target') const findRow = (clientY) => {
.forEach(r => r.classList.remove('drag-target')); for (const d of dayRows) {
if (clientY >= d.top && clientY <= d.bottom) return d;
const onMove = (ev) => { }
const dx = ev.clientX - startX; // Clamp to nearest row when pointer is between rows or outside canvas
const dy = ev.clientY - startY; if (!dayRows.length) return null;
ghost.style.left = (elRect.left + dx) + 'px'; if (clientY < dayRows[0].top) return dayRows[0];
ghost.style.top = (elRect.top + dy) + 'px'; return dayRows[dayRows.length - 1];
// 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');
}; };
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) => { const onUp = (ev) => {
document.removeEventListener('pointermove', onMove); document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp); 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(); ghost.remove();
el.style.opacity = ''; el.style.opacity = '';
clearHighlights(); 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(); App.renderCanvas();
}; };