feat: v4.3.0 - cross-day drag (blocks can move between day rows)
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."""
|
||||
|
||||
VERSION = "4.2.0"
|
||||
VERSION = "4.3.0"
|
||||
MAX_FILE_SIZE_MB = 10
|
||||
DEFAULT_COLOR = "#ffffff"
|
||||
|
||||
@@ -765,6 +765,20 @@ body {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
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 {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="header-title">Scénář Creator</h1>
|
||||
<span class="header-version">v4.2</span>
|
||||
<span class="header-version">v4.3</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<label class="btn btn-secondary btn-sm" id="importJsonBtn">
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="tab-content hidden" id="tab-docs">
|
||||
<div class="docs-container">
|
||||
<h2>Scénář Creator — Dokumentace</h2>
|
||||
<p class="docs-version">Verze 4.2.0 | <a href="/docs" target="_blank">Swagger API</a> | <a href="/api/sample">Vzorový JSON</a></p>
|
||||
<p class="docs-version">Verze 4.3.0 | <a href="/docs" target="_blank">Swagger API</a> | <a href="/api/sample">Vzorový JSON</a></p>
|
||||
|
||||
<h3>Jak začít</h3>
|
||||
<ol>
|
||||
@@ -104,7 +104,8 @@
|
||||
<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í 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>Ú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>
|
||||
|
||||
@@ -259,22 +259,21 @@ const Canvas = {
|
||||
App.openBlockModal(block.id);
|
||||
});
|
||||
|
||||
// interact.js drag (horizontal only)
|
||||
// interact.js drag (X = time, Y = cross-day)
|
||||
if (window.interact) {
|
||||
interact(el)
|
||||
.draggable({
|
||||
axis: 'x',
|
||||
listeners: {
|
||||
move: (event) => this._onDragMove(event, block),
|
||||
end: (event) => this._onDragEnd(event, block),
|
||||
start: (event) => this._onDragStart(event, block),
|
||||
move: (event) => this._onDragMove(event, block),
|
||||
end: (event) => this._onDragEnd(event, block),
|
||||
},
|
||||
modifiers: [
|
||||
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,
|
||||
relativePoints: [{ x: 0, y: 0 }]
|
||||
relativePoints: [{ x: 0, y: 0 }],
|
||||
}),
|
||||
interact.modifiers.restrict({ restriction: 'parent' })
|
||||
],
|
||||
inertia: false,
|
||||
})
|
||||
@@ -300,29 +299,72 @@ const Canvas = {
|
||||
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) {
|
||||
const target = event.target;
|
||||
const x = (parseFloat(target.style.left) || 0);
|
||||
// Convert delta px to minutes
|
||||
const deltaPx = event.dx;
|
||||
const deltaMin = deltaPx / this.pxPerMinute;
|
||||
const newStartMin = this.snapMinutes(this.parseTime(block.start) + deltaMin);
|
||||
|
||||
// ── X axis: update time ──────────────────────────────────────
|
||||
const deltaMin = event.dx / this.pxPerMinute;
|
||||
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 newEnd = clampedStart + duration;
|
||||
|
||||
block.start = this.formatTime(clampedStart);
|
||||
block.end = this.formatTime(newEnd);
|
||||
|
||||
block.end = this.formatTime(clampedStart + duration);
|
||||
const totalRange = this._endMin - this._startMin;
|
||||
const leftPct = (clampedStart - this._startMin) / totalRange * 100;
|
||||
target.style.left = leftPct + '%';
|
||||
target.style.left = ((clampedStart - this._startMin) / totalRange * 100) + '%';
|
||||
|
||||
// ── 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) {
|
||||
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();
|
||||
},
|
||||
|
||||
_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) {
|
||||
const target = event.target;
|
||||
const newWidthPx = event.rect.width;
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
from app.config import VERSION
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -48,7 +49,7 @@ def test_health(client):
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["version"] == "4.2.0"
|
||||
assert data["version"] == VERSION
|
||||
|
||||
|
||||
def test_root_returns_html(client):
|
||||
|
||||
Reference in New Issue
Block a user