mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 19:32:21 +02:00
refactor(notes): decompose Notes into feature-owned modules (#6713)
* 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>
This commit is contained in:
co-authored by
Léo
parent
9f8eae72fd
commit
f1a47f3812
@@ -0,0 +1,214 @@
|
||||
/** Mobile card long press, placeholder drag and reorder persistence. */
|
||||
export function createMobileReorder({
|
||||
lifecycle,
|
||||
_applyMasonry,
|
||||
_enterSelectMode,
|
||||
commitMobileOrder,
|
||||
app,
|
||||
}) {
|
||||
function _bindLongPressDrag(card) {
|
||||
let pressTimer = null;
|
||||
let startX = 0, startY = 0;
|
||||
let armed = false;
|
||||
const CANCEL_PX = 8;
|
||||
const HOLD_MS = 450;
|
||||
|
||||
card.addEventListener('touchstart', (e) => {
|
||||
// Don't fight scroll on touchpoints over real interactive children
|
||||
// (in mobile-mode they're CSS-hidden anyway, but be defensive).
|
||||
if (e.target.closest('button, input, a, .note-form')) return;
|
||||
if (e.touches.length !== 1) return;
|
||||
armed = true;
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
pressTimer = setTimeout(() => {
|
||||
if (!armed) return;
|
||||
try { navigator.vibrate?.(15); } catch {}
|
||||
app.setSuppressNextNoteTapId(card.dataset.noteId);
|
||||
_enterSelectMode(card.dataset.noteId);
|
||||
}, HOLD_MS);
|
||||
}, { passive: true });
|
||||
card.addEventListener('touchmove', (e) => {
|
||||
if (!armed) return;
|
||||
const t = e.touches[0];
|
||||
if (Math.abs(t.clientX - startX) > CANCEL_PX || Math.abs(t.clientY - startY) > CANCEL_PX) {
|
||||
armed = false;
|
||||
if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; }
|
||||
}
|
||||
}, { passive: true });
|
||||
const cancel = () => {
|
||||
armed = false;
|
||||
if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; }
|
||||
};
|
||||
lifecycle.onDispose(card, cancel);
|
||||
card.addEventListener('touchend', cancel, { passive: true });
|
||||
card.addEventListener('touchcancel', cancel, { passive: true });
|
||||
}
|
||||
|
||||
let _dragState = null;
|
||||
|
||||
let _docDragHandlersBound = false;
|
||||
|
||||
function _enterDragMode(initialCard, initialTouch) {
|
||||
document.body.classList.add('notes-drag-mode');
|
||||
document.querySelectorAll('.note-card').forEach(_setupDragForCard);
|
||||
if (!_docDragHandlersBound) {
|
||||
lifecycle.listen(document.getElementById('notes-pane'), document, 'touchmove', _onDocTouchMove, { passive: false });
|
||||
lifecycle.listen(document.getElementById('notes-pane'), document, 'touchend', _onDocTouchEnd, { passive: true });
|
||||
lifecycle.listen(document.getElementById('notes-pane'), document, 'touchcancel', _onDocTouchEnd, { passive: true });
|
||||
_docDragHandlersBound = true;
|
||||
}
|
||||
// Auto-grab the card the user long-pressed : they're already holding
|
||||
// their finger on it, so begin the drag straight away. Releasing
|
||||
// (touchend) commits the reorder AND exits drag mode in one motion.
|
||||
if (initialCard && initialTouch) {
|
||||
_beginGrab(initialCard, initialTouch);
|
||||
}
|
||||
}
|
||||
|
||||
function _exitDragMode() {
|
||||
document.body.classList.remove('notes-drag-mode');
|
||||
if (_dragState) {
|
||||
// Defensive: if exit fires while a drag is in flight, snap the card back.
|
||||
_onDocTouchEnd();
|
||||
}
|
||||
// Leave _docDragHandlersBound true so re-entering drag mode reuses them.
|
||||
}
|
||||
|
||||
function _setupDragForCard(card) {
|
||||
if (card.dataset.dragBound === '1') return;
|
||||
card.dataset.dragBound = '1';
|
||||
card.addEventListener('touchstart', (e) => {
|
||||
if (!document.body.classList.contains('notes-drag-mode')) return;
|
||||
if (e.touches.length !== 1) return;
|
||||
if (_dragState) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
_beginGrab(card, e.touches[0]);
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
function _beginGrab(card, touch) {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const prevStyle = card.getAttribute('style') || '';
|
||||
// Placeholder fills the card's old slot so the grid layout doesn't reflow.
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'note-card-placeholder';
|
||||
placeholder.style.width = rect.width + 'px';
|
||||
placeholder.style.height = rect.height + 'px';
|
||||
placeholder.style.margin = getComputedStyle(card).margin;
|
||||
if (card.style.gridRowEnd) placeholder.style.gridRowEnd = card.style.gridRowEnd;
|
||||
const grid = card.parentNode;
|
||||
grid.insertBefore(placeholder, card);
|
||||
|
||||
// Detach the card visually : fixed-position, anchored to the finger.
|
||||
card.classList.add('note-card-dragging');
|
||||
card.style.position = 'fixed';
|
||||
card.style.left = rect.left + 'px';
|
||||
card.style.top = rect.top + 'px';
|
||||
card.style.width = rect.width + 'px';
|
||||
card.style.height = rect.height + 'px';
|
||||
card.style.margin = '0';
|
||||
card.style.zIndex = '10001';
|
||||
// pointer-events:none so elementFromPoint sees the card BENEATH the finger
|
||||
card.style.pointerEvents = 'none';
|
||||
|
||||
_dragState = {
|
||||
card, placeholder, grid, prevStyle,
|
||||
grabOffsetX: touch.clientX - rect.left,
|
||||
grabOffsetY: touch.clientY - rect.top,
|
||||
};
|
||||
try { navigator.vibrate?.(8); } catch {}
|
||||
}
|
||||
|
||||
function _onDocTouchMove(e) {
|
||||
if (!_dragState) return;
|
||||
if (e.touches.length !== 1) return;
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const { card, placeholder, grid } = _dragState;
|
||||
card.style.left = (touch.clientX - _dragState.grabOffsetX) + 'px';
|
||||
const quickAdd = grid.querySelector('.notes-quick-add');
|
||||
const minTop = quickAdd ? quickAdd.getBoundingClientRect().bottom + 4 : grid.getBoundingClientRect().top;
|
||||
const maxTop = Math.max(minTop, window.innerHeight - card.getBoundingClientRect().height - 8);
|
||||
const nextTop = Math.max(minTop, Math.min(maxTop, touch.clientY - _dragState.grabOffsetY));
|
||||
card.style.top = nextTop + 'px';
|
||||
|
||||
const hitY = Math.max(minTop + 1, Math.min(window.innerHeight - 1, touch.clientY));
|
||||
const under = document.elementFromPoint(touch.clientX, hitY);
|
||||
const target = under && under.closest
|
||||
? under.closest('.note-card:not(.note-card-dragging)')
|
||||
: null;
|
||||
if (!target || target === card) return;
|
||||
if (target.parentNode !== grid) return;
|
||||
|
||||
// Move the PLACEHOLDER (not the card) above or below the target depending
|
||||
// on which half of the target the finger is in. This is the hysteresis
|
||||
// that stops the oscillation : once the placeholder moves past a card,
|
||||
// the cursor has to cross THAT card's midpoint in the other direction
|
||||
// to swap back.
|
||||
const tRect = target.getBoundingClientRect();
|
||||
const targetMidY = tRect.top + tRect.height / 2;
|
||||
if (touch.clientY < targetMidY) {
|
||||
if (placeholder.nextElementSibling !== target) {
|
||||
grid.insertBefore(placeholder, target);
|
||||
}
|
||||
} else {
|
||||
if (target.nextElementSibling !== placeholder) {
|
||||
grid.insertBefore(placeholder, target.nextElementSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _onDocTouchEnd() {
|
||||
if (!_dragState) return;
|
||||
const { card, placeholder, grid, prevStyle } = _dragState;
|
||||
_dragState = null;
|
||||
// Animate the card from its current fixed position to where the
|
||||
// placeholder sits, then re-parent and clear inline styles. Drag
|
||||
// mode auto-exits once the snap finishes : release = done.
|
||||
const phRect = placeholder.getBoundingClientRect();
|
||||
card.style.transition = 'left 0.2s ease, top 0.2s ease';
|
||||
card.style.left = phRect.left + 'px';
|
||||
card.style.top = phRect.top + 'px';
|
||||
setTimeout(() => {
|
||||
placeholder.parentNode.insertBefore(card, placeholder);
|
||||
placeholder.remove();
|
||||
card.classList.remove('note-card-dragging');
|
||||
// Restore the card's pre-drag inline styles. Mobile masonry stores
|
||||
// grid-row-end inline, and custom backgrounds use inline style too; wiping
|
||||
// cssText made dropped cards collapse into neighboring notes in grid view.
|
||||
if (prevStyle) card.setAttribute('style', prevStyle);
|
||||
else card.removeAttribute('style');
|
||||
_applyMasonry(grid);
|
||||
_commitNoteReorder();
|
||||
// One drag, one exit : release ends the rearrange session entirely.
|
||||
if (document.body.classList.contains('notes-drag-mode')) {
|
||||
document.body.classList.remove('notes-drag-mode');
|
||||
}
|
||||
}, 210);
|
||||
}
|
||||
|
||||
async function _commitNoteReorder() {
|
||||
const grid = document.querySelector('#notes-pane .notes-pane-body');
|
||||
if (!grid) return;
|
||||
const ids = Array.from(grid.querySelectorAll('.note-card')).map(c => c.dataset.noteId).filter(Boolean);
|
||||
if (!ids.length) return;
|
||||
await commitMobileOrder(ids);
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
if (_dragState) {
|
||||
const { card, placeholder, prevStyle } = _dragState;
|
||||
_dragState = null;
|
||||
placeholder.parentNode?.insertBefore(card, placeholder);
|
||||
placeholder.remove();
|
||||
card.classList.remove('note-card-dragging');
|
||||
if (prevStyle) card.setAttribute('style', prevStyle);
|
||||
else card.removeAttribute('style');
|
||||
}
|
||||
document.body.classList.remove('notes-drag-mode');
|
||||
_docDragHandlersBound = false;
|
||||
}
|
||||
return { dispose, _bindLongPressDrag, _enterDragMode, _exitDragMode, _setupDragForCard, _beginGrab, _onDocTouchMove, _onDocTouchEnd, _commitNoteReorder };
|
||||
}
|
||||
Reference in New Issue
Block a user