mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 11:22:21 +02:00
* refactor(notes): decompose Notes into feature-owned modules * fix(notes): avoid HTML interpolation in reminder picker * docs(notes): drop checkpoint archives, session logs and publication drafts The N0-N6 tarballs and raw pytest/node logs are not read by any script or test, and the logs carry absolute home-directory paths. Once squashed into dev they would stay in history for good. notes-pr.md and notes-issue.md were the drafts used to open #6712 and #6713, not documentation. notes-validation.md keeps the counts, the checkpoint hashes and implementation.sha256 as the record; local interpreter paths and the local Docker image name are replaced with portable placeholders. * fix(notes): keep the state store raw and stop masonry re-observing after close - state: writes built from views (filter, spread, splice) stored nested read-only views in the store, so structuredClone on a stored note threw DataCloneError. Store writes now unwrap nested views. - layout: a save that resolves after closePanel re-renders the leaving pane and re-created the masonry ResizeObserver that teardown had disposed. Skip creating it once Notes is closed. - index.js: drop 30 imported or declared names that were never used and group the imports at the top. Import order is unchanged. - Revert unrelated punctuation edits in .gitattributes and keep the header comment and docstring first in compare/vote.js and test_notes_dom_xss_helpers.py. Both fixes have a node test that fails on the previous head. * test(notes): make script rejection assertion case-insensitive Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum. --------- Co-authored-by: Léo <leograndcontact@gmail.com>
182 lines
8.2 KiB
JavaScript
182 lines
8.2 KiB
JavaScript
/** Desktop and large-touch-device card reorder gestures. */
|
|
export function createCardReorder({
|
|
lifecycle,
|
|
_isNotesMobileMode,
|
|
persistOrder,
|
|
state
|
|
}) {
|
|
function _wireCardReorder(body) {
|
|
lifecycle.release(body);
|
|
// Drag-reorder notes on pointer/mouse devices. Mobile uses the custom
|
|
// placeholder sorter below `_bindLongPressDrag`; native HTML5 dragging is
|
|
// unreliable on touch browsers and can compete with the long-press flow.
|
|
if (!_isNotesMobileMode()) {
|
|
body.querySelectorAll('.note-card').forEach(card => {
|
|
card.addEventListener('dragstart', (e) => {
|
|
if (e.target.closest('.note-checkbox, .note-card-x, .note-card-select, .note-card-pin, .note-card-action, .note-card-color-dot, .note-card-title, .note-card-edit, .note-card-edit-corner, .note-card-done, .note-card-corner-menu, .note-agent-tag, .note-card-label-chip')) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
card.classList.add('dragging');
|
|
body.classList.add('drag-active');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
try { e.dataTransfer.setData('text/plain', card.dataset.noteId); } catch {}
|
|
});
|
|
card.addEventListener('dragend', async () => {
|
|
card.classList.remove('dragging');
|
|
body.classList.remove('drag-active');
|
|
body.querySelectorAll('.drop-before, .drop-after').forEach(el => el.classList.remove('drop-before', 'drop-after'));
|
|
const ids = [...body.querySelectorAll('.note-card')].map(c => c.dataset.noteId);
|
|
try { await persistOrder(ids); }
|
|
catch {}
|
|
});
|
|
});
|
|
}
|
|
// Track which card we last swapped with so a single hover-over triggers
|
|
// one swap, not a jitter as the pointer keeps moving inside the same card.
|
|
let _lastSwapId = null;
|
|
function _maybeSwap(dragging, clientX, clientY) {
|
|
const target = document.elementFromPoint(clientX, clientY)?.closest('.note-card');
|
|
if (!target || target === dragging || !body.contains(target)) return;
|
|
const id = target.dataset.noteId;
|
|
if (id === _lastSwapId) return;
|
|
// FLIP across ALL siblings. In list view only `target` moves visually, but
|
|
// in grid view (2-col) and when the pinned-section grid-column-start rule
|
|
// shifts, several cards reflow at once. Capture every card's pre-swap rect,
|
|
// do the DOM swap, then animate any that actually moved. The dragging card
|
|
// is excluded : it's already finger-tracked via translate3d.
|
|
const cards = [...body.querySelectorAll('.note-card')].filter(c => c !== dragging);
|
|
const prevRects = new Map(cards.map(c => [c, c.getBoundingClientRect()]));
|
|
const draggingNext = dragging.nextSibling === target ? dragging : dragging.nextSibling;
|
|
body.insertBefore(dragging, target);
|
|
body.insertBefore(target, draggingNext);
|
|
for (const c of cards) {
|
|
const prev = prevRects.get(c);
|
|
const next = c.getBoundingClientRect();
|
|
const dx = prev.left - next.left;
|
|
const dy = prev.top - next.top;
|
|
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue;
|
|
c.style.transition = 'none';
|
|
c.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
requestAnimationFrame(() => {
|
|
c.style.transition = 'transform 0.22s cubic-bezier(0.34, 1.2, 0.64, 1)';
|
|
c.style.transform = '';
|
|
c.addEventListener('transitionend', () => { c.style.transition = ''; }, { once: true });
|
|
});
|
|
}
|
|
_lastSwapId = id;
|
|
}
|
|
lifecycle.listen(body, body, 'dragover', (e) => {
|
|
e.preventDefault();
|
|
const dragging = body.querySelector('.note-card.dragging');
|
|
if (!dragging) return;
|
|
_maybeSwap(dragging, e.clientX, e.clientY);
|
|
});
|
|
lifecycle.listen(body, body, 'dragend', () => { _lastSwapId = null; });
|
|
|
|
// Legacy touch drag for larger touch devices only. Phone-sized Notes uses
|
|
// the placeholder sorter wired by `_bindLongPressDrag`; running both flows
|
|
// makes one press start two independent drag sessions.
|
|
if (!_isNotesMobileMode() && 'ontouchstart' in window) {
|
|
let dragCard = null;
|
|
let isDragging = false;
|
|
let longPressTimer = null;
|
|
let startX = 0, startY = 0;
|
|
const LONG_PRESS_MS = 350;
|
|
const MOVE_THRESHOLD_PX = 8;
|
|
const _selectorSkip = '.note-checkbox, .note-card-x, .note-card-select, .note-card-pin, .note-card-action, .note-card-color-dot, .note-card-title, .note-card-edit, .note-card-edit-corner, .note-card-done, .note-card-corner-menu, .note-agent-tag, .note-card-label-chip, input, textarea, button, a';
|
|
|
|
// Anchor for the finger-follow transform. Recomputed after every swap so
|
|
// the card stays under the finger across reorderings.
|
|
let anchorX = 0, anchorY = 0;
|
|
const _follow = (clientX, clientY) => {
|
|
if (!dragCard) return;
|
|
const dx = clientX - anchorX;
|
|
const dy = clientY - anchorY;
|
|
// Compose with the CSS .dragging transform (scale + rotate).
|
|
dragCard.style.transform = `translate3d(${dx}px, ${dy}px, 0) scale(1.03) rotate(-0.6deg)`;
|
|
};
|
|
const _reanchor = (clientX, clientY) => {
|
|
if (!dragCard) return;
|
|
// Clear any prior translate so the card resettles in its natural slot,
|
|
// then anchor at the finger's current position. Subsequent _follow calls
|
|
// translate by (finger - anchor), keeping the finger at the same relative
|
|
// spot on the card it had when we re-anchored.
|
|
dragCard.style.transform = '';
|
|
anchorX = clientX;
|
|
anchorY = clientY;
|
|
};
|
|
|
|
const _endDrag = (committed) => {
|
|
clearTimeout(longPressTimer);
|
|
longPressTimer = null;
|
|
if (dragCard) {
|
|
dragCard.classList.remove('dragging');
|
|
dragCard.style.transform = '';
|
|
}
|
|
body.classList.remove('drag-active');
|
|
_lastSwapId = null;
|
|
if (isDragging) {
|
|
document.documentElement.style.touchAction = '';
|
|
if (committed) {
|
|
const ids = [...body.querySelectorAll('.note-card')].map(c => c.dataset.noteId);
|
|
persistOrder(ids).catch(() => {});
|
|
}
|
|
}
|
|
dragCard = null;
|
|
isDragging = false;
|
|
};
|
|
lifecycle.onDispose(body, () => _endDrag(false));
|
|
|
|
lifecycle.listen(body, body, 'touchstart', (e) => {
|
|
if (state.selectMode) return;
|
|
const card = e.target.closest('.note-card');
|
|
if (!card) return;
|
|
if (e.target.closest(_selectorSkip)) return;
|
|
const t = e.touches[0];
|
|
startX = t.clientX; startY = t.clientY;
|
|
dragCard = card;
|
|
longPressTimer = setTimeout(() => {
|
|
if (!dragCard) return;
|
|
isDragging = true;
|
|
dragCard.classList.add('dragging');
|
|
body.classList.add('drag-active');
|
|
document.documentElement.style.touchAction = 'none';
|
|
_reanchor(startX, startY);
|
|
try { if (navigator.vibrate) navigator.vibrate(15); } catch {}
|
|
}, LONG_PRESS_MS);
|
|
}, { passive: true });
|
|
|
|
lifecycle.listen(body, body, 'touchmove', (e) => {
|
|
if (!dragCard) return;
|
|
const t = e.touches[0];
|
|
if (!isDragging) {
|
|
// Movement before long-press fires = user is scrolling; cancel pickup.
|
|
if (Math.abs(t.clientX - startX) > MOVE_THRESHOLD_PX || Math.abs(t.clientY - startY) > MOVE_THRESHOLD_PX) {
|
|
clearTimeout(longPressTimer);
|
|
longPressTimer = null;
|
|
dragCard = null;
|
|
}
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
// Live-follow the finger first, then check for a swap. After a swap the
|
|
// card's natural slot moves, so we re-anchor and re-apply the offset.
|
|
_follow(t.clientX, t.clientY);
|
|
const before = dragCard.parentNode && [...dragCard.parentNode.children].indexOf(dragCard);
|
|
_maybeSwap(dragCard, t.clientX, t.clientY);
|
|
const after = dragCard.parentNode && [...dragCard.parentNode.children].indexOf(dragCard);
|
|
if (before !== after) {
|
|
_reanchor(t.clientX, t.clientY);
|
|
_follow(t.clientX, t.clientY);
|
|
}
|
|
}, { passive: false });
|
|
|
|
lifecycle.listen(body, body, 'touchend', () => _endDrag(true));
|
|
lifecycle.listen(body, body, 'touchcancel', () => _endDrag(false));
|
|
}
|
|
}
|
|
|
|
return { _wireCardReorder };
|
|
}
|