fix(layout): Phase-5-Review — off-screen-Clamp, Drag-Cleanup, Blur-Position, Import-pos
- Render + neuer debounced Resize-Handler clampen --board-x/y gegen den
aktuellen Viewport: ein auf breiterem Fenster platziertes Board rendert
nie mehr off-screen (und damit per Drag unerreichbar). board.pos bleibt
unveraendert, bei spaeterer Verbreiterung wird die Originalposition erreicht.
- drag.js: cleanup() + pointercancel-Listener. Die Klasse .board.dragging
klebte bei Touch-Interrupt/Browser-Geste sonst dauerhaft und legte den
app.js-Sync-Guard (Quick-Save-Render) still.
- main.css: '.board.blurred { position: relative }' entfernt — lag im
utilities-Layer und schlug das absolute Free-Layout (geblurrtes Board fiel
aus seiner Position + war nicht mehr drag-bar).
- data.js: board.pos wird beim JSON-Import durchgereicht (safePos-Validierung
via Number.isFinite), sonst Verlust des frei gesetzten Layouts beim Restore.
This commit is contained in:
+4
-3
@@ -910,9 +910,10 @@ body.show-desc .bm-desc { display: block; }
|
||||
.board.blurred .board-title {
|
||||
filter: blur(5px);
|
||||
}
|
||||
.board.blurred {
|
||||
position: relative;
|
||||
}
|
||||
/* HINWEIS: KEIN `.board.blurred { position: relative }` — .board ist im Free-Layout bereits
|
||||
position:absolute (= positionierter Containing-Block fuers Overlay). Ein relative hier liegt
|
||||
im @layer utilities und wuerde das absolute schlagen -> geblurrtes Board faellt aus seiner
|
||||
freien Position + waere nicht mehr drag-bar (Phase-5-Review). */
|
||||
.board-blur-overlay {
|
||||
display: none;
|
||||
position: absolute; inset: 0; z-index: 5;
|
||||
|
||||
@@ -27,6 +27,7 @@ async function init() {
|
||||
bindGlobalEvents();
|
||||
bindSettingsEvents();
|
||||
bindStorageSync();
|
||||
bindBoardResizeReclamp(); // Boards bei Fenster-Verkleinerung wieder in den sichtbaren Bereich holen
|
||||
await drainQuickSavePending(); // beim Start angesammelte Quick-Saves (kein Tab war offen) einlesen
|
||||
initSearch();
|
||||
initPalette();
|
||||
@@ -282,4 +283,20 @@ function bindStorageSync() {
|
||||
});
|
||||
}
|
||||
|
||||
// Freies Layout (LAYOUT-04): Boards stehen absolut positioniert. Schrumpft das Fenster, koennen
|
||||
// sie ganz aus dem sichtbaren Bereich rutschen. renderBoards() klemmt die --board-x/--board-y jedes
|
||||
// Boards beim Aufbau gegen die aktuelle Viewport — ein simpler Re-Render holt sie also zurueck.
|
||||
// Debounce (150ms), damit kontinuierliches Resizen nicht hunderte Renders ausloest. Waehrend eines
|
||||
// aktiven Drags NICHT neu rendern: renderBoards->replaceChildren wuerde den laufenden Drag abreissen.
|
||||
function bindBoardResizeReclamp() {
|
||||
let resizeTimer = null;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (document.querySelector('.board.dragging, .bm-item.dragging-source')) return;
|
||||
renderBoards();
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
+8
-4
@@ -96,11 +96,15 @@ function renderBoards() {
|
||||
|
||||
boards.forEach(board => {
|
||||
const el = createBoardEl(board);
|
||||
// Position als Custom-Property setzen (nicht inline left/top), damit der
|
||||
// Mobil-@media-Reset sie ueberschreiben kann.
|
||||
el.style.setProperty('--board-x', board.pos.x + 'px');
|
||||
el.style.setProperty('--board-y', board.pos.y + 'px');
|
||||
wrapper.appendChild(el);
|
||||
// Position als Custom-Property setzen (nicht inline left/top), damit der Mobil-@media-Reset
|
||||
// sie ueberschreiben kann. Gegen den AKTUELLEN Viewport clampen, damit ein auf breiterem
|
||||
// Fenster platziertes Board nie off-screen (und damit per Drag unerreichbar) rendert.
|
||||
// board.pos bleibt unveraendert -> bei spaeterer Verbreiterung wird die Originalposition wieder erreicht.
|
||||
const cx = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, board.pos.x));
|
||||
const cy = Math.max(48, Math.min(window.innerHeight - el.offsetHeight, board.pos.y));
|
||||
el.style.setProperty('--board-x', cx + 'px');
|
||||
el.style.setProperty('--board-y', cy + 'px');
|
||||
});
|
||||
initBoardDragDrop();
|
||||
|
||||
|
||||
+32
-13
@@ -24,6 +24,17 @@ function initDataButtons() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert eine freie Layout-Position (LAYOUT-04). Liefert { x, y } nur bei
|
||||
* endlichen Zahlen, sonst null — dann gridded ensureBoardPositions das Board neu.
|
||||
* Ohne das wuerde ein Import jede vom Nutzer gesetzte Board-Position verwerfen.
|
||||
* @param {*} pos
|
||||
* @returns {{x:number,y:number}|null}
|
||||
*/
|
||||
function safePos(pos) {
|
||||
return pos && Number.isFinite(pos.x) && Number.isFinite(pos.y) ? { x: pos.x, y: pos.y } : null;
|
||||
}
|
||||
|
||||
// Export (inkl. Notes)
|
||||
btnExport.addEventListener('click', async () => {
|
||||
const widgetData = await Store.get('widgetStates');
|
||||
@@ -56,19 +67,25 @@ function initDataButtons() {
|
||||
if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format'));
|
||||
const validBoards = data.boards
|
||||
.filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
|
||||
.map(b => ({
|
||||
id: b.id || uid(),
|
||||
title: String(b.title).slice(0, 100),
|
||||
blurred: !!b.blurred,
|
||||
bookmarks: b.bookmarks
|
||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||
.map(bm => ({
|
||||
id: bm.id || uid(),
|
||||
title: String(bm.title).slice(0, 200),
|
||||
url: bm.url,
|
||||
desc: String(bm.desc || '').slice(0, 500)
|
||||
}))
|
||||
}));
|
||||
.map(b => {
|
||||
const board = {
|
||||
id: b.id || uid(),
|
||||
title: String(b.title).slice(0, 100),
|
||||
blurred: !!b.blurred,
|
||||
bookmarks: b.bookmarks
|
||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||
.map(bm => ({
|
||||
id: bm.id || uid(),
|
||||
title: String(bm.title).slice(0, 200),
|
||||
url: bm.url,
|
||||
desc: String(bm.desc || '').slice(0, 500)
|
||||
}))
|
||||
};
|
||||
// Freies Layout (LAYOUT-04): valide Position uebernehmen, sonst gridded ensureBoardPositions neu.
|
||||
const pos = safePos(b.pos);
|
||||
if (pos) board.pos = pos;
|
||||
return board;
|
||||
});
|
||||
if (validBoards.length === 0) throw new Error(t('data.no_boards'));
|
||||
const ok = await HellionDialog.confirm(
|
||||
t('data.import_confirm', { count: validBoards.length }),
|
||||
@@ -92,6 +109,8 @@ function initDataButtons() {
|
||||
id: e.item.id || uid(),
|
||||
title: String(e.item.title || '').slice(0, 100),
|
||||
blurred: !!e.item.blurred,
|
||||
// Position erhalten, damit ein wiederhergestelltes Board an seinem Platz landet.
|
||||
...(safePos(e.item.pos) ? { pos: safePos(e.item.pos) } : {}),
|
||||
bookmarks: Array.isArray(e.item.bookmarks)
|
||||
? e.item.bookmarks
|
||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||
|
||||
+15
-3
@@ -41,12 +41,19 @@ function initBoardDragDrop() {
|
||||
boardEl.style.setProperty('--board-y', y + 'px');
|
||||
}
|
||||
|
||||
async function onUp() {
|
||||
handle.releasePointerCapture(e.pointerId);
|
||||
// Gemeinsames Aufraeumen: Pointer-Capture freigeben, ALLE Listener entfernen,
|
||||
// .board.dragging entfernen. MUSS auch im Cancel-Pfad laufen — sonst klebt die Klasse
|
||||
// und der app.js-Sync-Guard unterdrueckt dauerhaft Quick-Save-Renders (Phase-5-Review).
|
||||
function cleanup() {
|
||||
try { handle.releasePointerCapture(e.pointerId); } catch (_) { /* schon freigegeben */ }
|
||||
handle.removeEventListener('pointermove', onMove);
|
||||
handle.removeEventListener('pointerup', onUp);
|
||||
boardEl.classList.remove('dragging'); // z-index zurueck + Live-Sync-Guard freigeben
|
||||
handle.removeEventListener('pointercancel', onCancel);
|
||||
boardEl.classList.remove('dragging');
|
||||
}
|
||||
|
||||
async function onUp() {
|
||||
cleanup();
|
||||
const id = boardEl.dataset.boardId;
|
||||
const board = boards.find(b => b.id === id);
|
||||
if (board) {
|
||||
@@ -58,8 +65,13 @@ function initBoardDragDrop() {
|
||||
}
|
||||
}
|
||||
|
||||
// pointercancel feuert STATT pointerup bei Touch-Interrupt, Browser-Geste oder wenn das
|
||||
// captured Element aus dem DOM faellt -> nur aufraeumen (cleanup), pos NICHT ueberschreiben.
|
||||
function onCancel() { cleanup(); }
|
||||
|
||||
handle.addEventListener('pointermove', onMove);
|
||||
handle.addEventListener('pointerup', onUp);
|
||||
handle.addEventListener('pointercancel', onCancel);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user