feat: v4.3.0 - cross-day drag (blocks can move between day rows)
Some checks failed
Build & Push Docker / build (push) Has been cancelled

This commit is contained in:
2026-02-20 18:36:51 +01:00
parent f3e2ae2cda
commit 751ffe6f82
5 changed files with 81 additions and 23 deletions

View File

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

View File

@@ -765,6 +765,20 @@ body {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
overflow: hidden; overflow: hidden;
position: relative;
}
/* Allow blocks to visually leave their row during cross-day drag */
.day-rows.dragging .day-row,
.day-rows.dragging .day-timeline {
overflow: visible;
}
/* Highlight the row being dragged over */
.day-timeline.drag-target {
background: #eff6ff;
outline: 2px solid var(--accent);
outline-offset: -2px;
} }
.day-label { .day-label {

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.2</span> <span class="header-version">v4.3</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">
@@ -84,7 +84,7 @@
<div class="tab-content hidden" id="tab-docs"> <div class="tab-content hidden" id="tab-docs">
<div class="docs-container"> <div class="docs-container">
<h2>Scénář Creator — Dokumentace</h2> <h2>Scénář Creator — Dokumentace</h2>
<p class="docs-version">Verze 4.2.0 &nbsp;|&nbsp; <a href="/docs" target="_blank">Swagger API</a> &nbsp;|&nbsp; <a href="/api/sample">Vzorový JSON</a></p> <p class="docs-version">Verze 4.3.0 &nbsp;|&nbsp; <a href="/docs" target="_blank">Swagger API</a> &nbsp;|&nbsp; <a href="/api/sample">Vzorový JSON</a></p>
<h3>Jak začít</h3> <h3>Jak začít</h3>
<ol> <ol>
@@ -104,7 +104,8 @@
<ul> <ul>
<li><strong>Přidání:</strong> Klikněte na „+ Přidat blok" nebo klikněte na prázdné místo v řádku dne.</li> <li><strong>Přidání:</strong> Klikněte na „+ Přidat blok" nebo klikněte na prázdné místo v řádku dne.</li>
<li><strong>Přidání do všech dnů:</strong> V modalu nového bloku zaškrtněte „Přidat do každého dne kurzu" — vytvoří identický blok pro každý den akce (sdílená série).</li> <li><strong>Přidání do všech dnů:</strong> V modalu nového bloku zaškrtněte „Přidat do každého dne kurzu" — vytvoří identický blok pro každý den akce (sdílená série).</li>
<li><strong>Přesun:</strong> Chytněte blok a táhněte doleva/doprava. Snap na 15 minut.</li> <li><strong>Přesun v rámci dne:</strong> Chytněte blok a táhněte doleva/doprava. Snap na 15 minut.</li>
<li><strong>Přesun mezi dny:</strong> Táhněte blok nahoru/dolů — zvýrazní se cílový den (modrý rámeček). Pusťte na novém dni.</li>
<li><strong>Změna délky:</strong> Chytněte pravý okraj bloku a táhněte.</li> <li><strong>Změna délky:</strong> Chytněte pravý okraj bloku a táhněte.</li>
<li><strong>Úprava:</strong> Klikněte na blok — otevře se formulář s editací jednoho bloku.</li> <li><strong>Úprava:</strong> Klikněte na blok — otevře se formulář s editací jednoho bloku.</li>
<li><strong>Smazání jednoho bloku:</strong> V editačním formuláři klikněte na „Smazat jen tento".</li> <li><strong>Smazání jednoho bloku:</strong> V editačním formuláři klikněte na „Smazat jen tento".</li>

View File

@@ -259,22 +259,21 @@ const Canvas = {
App.openBlockModal(block.id); App.openBlockModal(block.id);
}); });
// interact.js drag (horizontal only) // interact.js drag (X = time, Y = cross-day)
if (window.interact) { if (window.interact) {
interact(el) interact(el)
.draggable({ .draggable({
axis: 'x',
listeners: { listeners: {
start: (event) => this._onDragStart(event, block),
move: (event) => this._onDragMove(event, block), move: (event) => this._onDragMove(event, block),
end: (event) => this._onDragEnd(event, block), end: (event) => this._onDragEnd(event, block),
}, },
modifiers: [ modifiers: [
interact.modifiers.snap({ interact.modifiers.snap({
targets: [interact.snappers.grid({ x: this._minutesPx(this.GRID_MINUTES), y: 1000 })], targets: [interact.snappers.grid({ x: this._minutesPx(this.GRID_MINUTES), y: 10000 })],
range: Infinity, range: Infinity,
relativePoints: [{ x: 0, y: 0 }] relativePoints: [{ x: 0, y: 0 }],
}), }),
interact.modifiers.restrict({ restriction: 'parent' })
], ],
inertia: false, inertia: false,
}) })
@@ -300,29 +299,72 @@ const Canvas = {
return minutes * this.pxPerMinute; return minutes * this.pxPerMinute;
}, },
_onDragStart(event, block) {
const target = event.target;
target.dataset.dy = '0';
target.style.zIndex = '500';
// Enable cross-row overflow while dragging
const dayRows = document.getElementById('dayRows');
if (dayRows) dayRows.classList.add('dragging');
},
_onDragMove(event, block) { _onDragMove(event, block) {
const target = event.target; const target = event.target;
const x = (parseFloat(target.style.left) || 0);
// Convert delta px to minutes // ── X axis: update time ──────────────────────────────────────
const deltaPx = event.dx; const deltaMin = event.dx / this.pxPerMinute;
const deltaMin = deltaPx / this.pxPerMinute;
const newStartMin = this.snapMinutes(this.parseTime(block.start) + deltaMin);
const duration = this.parseTime(block.end) - this.parseTime(block.start); const duration = this.parseTime(block.end) - this.parseTime(block.start);
const newStartMin = this.snapMinutes(this.parseTime(block.start) + deltaMin);
const clampedStart = Math.max(this._startMin, Math.min(this._endMin - duration, newStartMin)); const clampedStart = Math.max(this._startMin, Math.min(this._endMin - duration, newStartMin));
const newEnd = clampedStart + duration;
block.start = this.formatTime(clampedStart); block.start = this.formatTime(clampedStart);
block.end = this.formatTime(newEnd); block.end = this.formatTime(clampedStart + duration);
const totalRange = this._endMin - this._startMin; const totalRange = this._endMin - this._startMin;
const leftPct = (clampedStart - this._startMin) / totalRange * 100; target.style.left = ((clampedStart - this._startMin) / totalRange * 100) + '%';
target.style.left = leftPct + '%';
// ── Y axis: visual shift + row highlight ─────────────────────
const dy = (parseFloat(target.dataset.dy) || 0) + event.dy;
target.dataset.dy = dy;
target.style.transform = `translateY(${dy}px)`;
this._highlightTargetRow(target);
}, },
_onDragEnd(event, block) { _onDragEnd(event, block) {
const target = event.target;
// Detect which day-timeline the block's vertical center is over
const rect = target.getBoundingClientRect();
const centerY = rect.top + rect.height / 2;
for (const row of document.querySelectorAll('.day-timeline')) {
const rr = row.getBoundingClientRect();
if (centerY >= rr.top && centerY <= rr.bottom) {
block.date = row.dataset.date;
break;
}
}
// Cleanup
document.querySelectorAll('.day-timeline.drag-target')
.forEach(r => r.classList.remove('drag-target'));
const dayRows = document.getElementById('dayRows');
if (dayRows) dayRows.classList.remove('dragging');
target.style.transform = '';
target.style.zIndex = '';
delete target.dataset.dy;
App.renderCanvas(); App.renderCanvas();
}, },
_highlightTargetRow(el) {
const rect = el.getBoundingClientRect();
const centerY = rect.top + rect.height / 2;
document.querySelectorAll('.day-timeline').forEach(row => {
const rr = row.getBoundingClientRect();
const isTarget = centerY >= rr.top && centerY <= rr.bottom;
row.classList.toggle('drag-target', isTarget);
});
},
_onResizeMove(event, block) { _onResizeMove(event, block) {
const target = event.target; const target = event.target;
const newWidthPx = event.rect.width; const newWidthPx = event.rect.width;

View File

@@ -6,6 +6,7 @@ import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from app.main import app from app.main import app
from app.config import VERSION
@pytest.fixture @pytest.fixture
@@ -48,7 +49,7 @@ def test_health(client):
assert r.status_code == 200 assert r.status_code == 200
data = r.json() data = r.json()
assert data["status"] == "ok" assert data["status"] == "ok"
assert data["version"] == "4.2.0" assert data["version"] == VERSION
def test_root_returns_html(client): def test_root_returns_html(client):