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
Some checks failed
Build & Push Docker / build (push) Has been cancelled
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user