fix: v4.5.0 - cross-day drag (ghost element, no overflow issue); adaptive block labels; PDF fit_text truncation
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:
@@ -229,28 +229,35 @@ const Canvas = {
|
||||
position:absolute;
|
||||
`;
|
||||
|
||||
// Block label
|
||||
// Block label — adaptive based on available width
|
||||
const widthPx = (ce - cs) * this.pxPerMinute;
|
||||
const inner = document.createElement('div');
|
||||
inner.className = 'block-inner';
|
||||
const timeLabel = `${block.start}–${block.end}`;
|
||||
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.className = 'block-title';
|
||||
nameEl.textContent = block.title + (block.notes ? ' *' : '');
|
||||
if (widthPx >= 28) {
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.className = 'block-title';
|
||||
nameEl.textContent = block.title + (block.notes ? ' *' : '');
|
||||
inner.appendChild(nameEl);
|
||||
|
||||
const timeEl = document.createElement('span');
|
||||
timeEl.className = 'block-time';
|
||||
timeEl.textContent = timeLabel + (isOvernight ? ' →' : '');
|
||||
if (widthPx >= 72) {
|
||||
const timeLabel = `${block.start}–${block.end}`;
|
||||
const timeEl = document.createElement('span');
|
||||
timeEl.className = 'block-time';
|
||||
timeEl.textContent = timeLabel + (isOvernight ? ' →' : '');
|
||||
inner.appendChild(timeEl);
|
||||
}
|
||||
|
||||
inner.appendChild(nameEl);
|
||||
inner.appendChild(timeEl);
|
||||
|
||||
if (block.responsible) {
|
||||
const respEl = document.createElement('span');
|
||||
respEl.className = 'block-responsible';
|
||||
respEl.textContent = block.responsible;
|
||||
inner.appendChild(respEl);
|
||||
if (widthPx >= 90 && block.responsible) {
|
||||
const respEl = document.createElement('span');
|
||||
respEl.className = 'block-responsible';
|
||||
respEl.textContent = block.responsible;
|
||||
inner.appendChild(respEl);
|
||||
}
|
||||
}
|
||||
// Tooltip always available for narrow blocks
|
||||
el.title = `${block.title} (${block.start}–${block.end})` +
|
||||
(block.responsible ? ` · ${block.responsible}` : '');
|
||||
el.appendChild(inner);
|
||||
|
||||
// Click to edit
|
||||
@@ -259,24 +266,18 @@ const Canvas = {
|
||||
App.openBlockModal(block.id);
|
||||
});
|
||||
|
||||
// interact.js drag (X = time, Y = cross-day)
|
||||
// Native pointer drag — ghost on document.body, avoids overflow/clipping issues
|
||||
el.addEventListener('pointerdown', (e) => {
|
||||
const elRect = el.getBoundingClientRect();
|
||||
if (e.clientX > elRect.right - 8) return; // right 8px = resize handle
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._startPointerDrag(e, el, block);
|
||||
});
|
||||
|
||||
// interact.js resize only (right edge)
|
||||
if (window.interact) {
|
||||
interact(el)
|
||||
.draggable({
|
||||
listeners: {
|
||||
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: 10000 })],
|
||||
range: Infinity,
|
||||
relativePoints: [{ x: 0, y: 0 }],
|
||||
}),
|
||||
],
|
||||
inertia: false,
|
||||
})
|
||||
.resizable({
|
||||
edges: { right: true },
|
||||
listeners: {
|
||||
@@ -299,70 +300,82 @@ 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');
|
||||
},
|
||||
// Native pointer drag — creates a ghost on document.body so clipping never happens
|
||||
_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);
|
||||
|
||||
_onDragMove(event, block) {
|
||||
const target = event.target;
|
||||
|
||||
// ── 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));
|
||||
block.start = this.formatTime(clampedStart);
|
||||
block.end = this.formatTime(clampedStart + duration);
|
||||
const totalRange = this._endMin - this._startMin;
|
||||
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);
|
||||
// Create floating ghost
|
||||
const ghost = document.createElement('div');
|
||||
ghost.className = 'block-el drag-ghost';
|
||||
ghost.innerHTML = el.innerHTML;
|
||||
Object.assign(ghost.style, {
|
||||
position: 'fixed',
|
||||
left: elRect.left + 'px',
|
||||
top: elRect.top + 'px',
|
||||
width: elRect.width + 'px',
|
||||
height: elRect.height + 'px',
|
||||
background: el.style.background,
|
||||
color: el.style.color,
|
||||
zIndex: '9999',
|
||||
opacity: '0.88',
|
||||
pointerEvents: 'none',
|
||||
cursor: 'grabbing',
|
||||
boxShadow: '0 6px 20px rgba(0,0,0,0.28)',
|
||||
borderRadius: '4px',
|
||||
transition: 'none',
|
||||
margin: '0',
|
||||
});
|
||||
document.body.appendChild(ghost);
|
||||
el.style.opacity = '0.25'; // dim original, don't hide (keeps layout)
|
||||
|
||||
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');
|
||||
};
|
||||
|
||||
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();
|
||||
App.renderCanvas();
|
||||
};
|
||||
|
||||
document.addEventListener('pointermove', onMove);
|
||||
document.addEventListener('pointerup', onUp);
|
||||
},
|
||||
|
||||
_onResizeMove(event, block) {
|
||||
|
||||
Reference in New Issue
Block a user