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,123 @@
|
||||
/** Card reflow animation and the owned masonry resize observer. */
|
||||
export function createLayout({ isOpen = () => true } = {}) {
|
||||
function _captureCardPositions() {
|
||||
const body = document.querySelector('#notes-pane .notes-pane-body');
|
||||
if (!body) return null;
|
||||
const positions = new Map();
|
||||
body.querySelectorAll('.note-card').forEach(card => {
|
||||
const id = card.dataset.noteId;
|
||||
if (id) positions.set(id, card.getBoundingClientRect());
|
||||
});
|
||||
return positions;
|
||||
}
|
||||
|
||||
function _animateReflow(prevPositions) {
|
||||
if (!prevPositions || !prevPositions.size) return;
|
||||
const body = document.querySelector('#notes-pane .notes-pane-body');
|
||||
if (!body || !body.isConnected) return;
|
||||
body.querySelectorAll('.note-card').forEach(card => {
|
||||
const id = card.dataset.noteId;
|
||||
const prev = prevPositions.get(id);
|
||||
if (!prev) return;
|
||||
const next = card.getBoundingClientRect();
|
||||
const dx = prev.left - next.left;
|
||||
const dy = prev.top - next.top;
|
||||
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
|
||||
// Invert: jump back to old position
|
||||
card.style.transition = 'none';
|
||||
card.style.transform = `translate(${dx}px, ${dy}px)`;
|
||||
// Play: animate to 0
|
||||
requestAnimationFrame(() => {
|
||||
card.style.transition = 'transform 0.25s cubic-bezier(0.34, 1.2, 0.64, 1)';
|
||||
card.style.transform = '';
|
||||
card.addEventListener('transitionend', () => {
|
||||
card.style.transition = '';
|
||||
}, { once: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _applyMasonry(body) {
|
||||
if (!body || !body.isConnected) return;
|
||||
const pane = body.closest('.notes-pane');
|
||||
const isGrid = pane?.classList.contains('notes-view-grid');
|
||||
const isMobileGrid = isGrid && window.matchMedia('(max-width: 768px)').matches;
|
||||
// Tear down any prior observer (defensive : _renderNotes wipes body.innerHTML).
|
||||
if (_masonryObserver) { try { _masonryObserver.disconnect(); } catch {} _masonryObserver = null; }
|
||||
// A save that resolves after close re-renders the leaving pane; don't
|
||||
// re-create the observer that panel teardown already disposed.
|
||||
if (!isOpen()) return;
|
||||
if (!isGrid) {
|
||||
// Clear any leftover inline spans so list view lays out normally.
|
||||
body.querySelectorAll('.note-card, .notes-label-toggle, .notes-labels-bar, .notes-quick-add, .note-form, .doclib-chip-scroll-frame:has(> .notes-labels-bar)').forEach(c => { c.style.gridRowEnd = ''; });
|
||||
return;
|
||||
}
|
||||
const ROW_PX = 4;
|
||||
const spanForHeight = (h) => Math.max(1, Math.ceil(h / ROW_PX));
|
||||
const recomputeFullRows = () => {
|
||||
const quickAdd = body.querySelector('.notes-quick-add');
|
||||
const labelsToggle = body.querySelector('.notes-label-toggle');
|
||||
const labelsBar = body.querySelector('.notes-labels-bar');
|
||||
const labelsLayout = labelsBar?.parentElement?.classList.contains('doclib-chip-scroll-frame')
|
||||
? labelsBar.parentElement
|
||||
: labelsBar;
|
||||
if (labelsToggle && getComputedStyle(labelsToggle).display !== 'none') {
|
||||
labelsToggle.style.gridRowEnd = `span ${Math.max(1, spanForHeight(labelsToggle.scrollHeight))}`;
|
||||
}
|
||||
if (labelsLayout && getComputedStyle(labelsLayout).display !== 'none') {
|
||||
const shave = isMobileGrid ? 4 : 0;
|
||||
labelsLayout.style.gridRowEnd = `span ${Math.max(1, spanForHeight(labelsLayout.scrollHeight) - shave)}`;
|
||||
}
|
||||
if (quickAdd) {
|
||||
const shave = isMobileGrid ? 4 : 0;
|
||||
quickAdd.style.gridRowEnd = `span ${Math.max(1, spanForHeight(quickAdd.scrollHeight + 10) - shave)}`;
|
||||
}
|
||||
body.querySelectorAll('.note-form').forEach(form => {
|
||||
form.style.gridColumn = '1 / -1';
|
||||
const isDrawForm = !!form.querySelector('.note-form-type-seg.is-draw');
|
||||
const minSpan = isMobileGrid ? (isDrawForm ? 104 : 64) : 1;
|
||||
const renderedHeight = form.getBoundingClientRect?.().height || 0;
|
||||
const drawReserve = isDrawForm && isMobileGrid ? 12 : 12;
|
||||
const measuredHeight = Math.max(form.scrollHeight, renderedHeight) + drawReserve;
|
||||
form.style.gridRowEnd = `span ${Math.max(minSpan, spanForHeight(measuredHeight))}`;
|
||||
});
|
||||
};
|
||||
const recompute = (card) => {
|
||||
// scrollHeight returns the natural content height : card.getBoundingClientRect()
|
||||
// would return the grid cell height (collapsed to 4px until the span is set,
|
||||
// which is the value we're trying to compute).
|
||||
const h = card.scrollHeight + (isMobileGrid ? 6 : 8);
|
||||
if (h <= 0) return;
|
||||
card.style.gridRowEnd = `span ${spanForHeight(h)}`;
|
||||
};
|
||||
recomputeFullRows();
|
||||
body.querySelectorAll('.note-card').forEach(recompute);
|
||||
// Watch masonry participants : content can grow (image load, todo edits,
|
||||
// quick-add/form expansion), and stale spans are what cause visual merging.
|
||||
if ('ResizeObserver' in window) {
|
||||
_masonryObserver = new ResizeObserver(entries => {
|
||||
let fullRowsChanged = false;
|
||||
for (const e of entries) {
|
||||
if (e.target.classList.contains('note-card')) recompute(e.target);
|
||||
else fullRowsChanged = true;
|
||||
}
|
||||
if (fullRowsChanged) recomputeFullRows();
|
||||
});
|
||||
body.querySelectorAll('.note-card').forEach(c => _masonryObserver.observe(c));
|
||||
body.querySelectorAll('.notes-label-toggle, .notes-quick-add, .note-form').forEach(c => _masonryObserver.observe(c));
|
||||
const labelsBar = body.querySelector('.notes-labels-bar');
|
||||
const labelsLayout = labelsBar?.parentElement?.classList.contains('doclib-chip-scroll-frame')
|
||||
? labelsBar.parentElement
|
||||
: labelsBar;
|
||||
if (labelsLayout) _masonryObserver.observe(labelsLayout);
|
||||
}
|
||||
}
|
||||
|
||||
let _masonryObserver = null;
|
||||
|
||||
function dispose() {
|
||||
_masonryObserver?.disconnect();
|
||||
_masonryObserver = null;
|
||||
}
|
||||
return { dispose, _captureCardPositions, _animateReflow, _applyMasonry };
|
||||
}
|
||||
Reference in New Issue
Block a user