mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-13 19:52:20 +02:00
152 lines
4.9 KiB
JavaScript
152 lines
4.9 KiB
JavaScript
// Shared horizontal scrolling for filter/tag chip rows.
|
|
|
|
const CHIP_STRIP_SELECTOR = [
|
|
'.skills-summary-strip',
|
|
'#memory-category-filters',
|
|
'.memory-category-filters:has(> .memory-cat-chip)',
|
|
'.doclib-lang-chips',
|
|
'.doclib-chips',
|
|
'.notes-labels-bar',
|
|
'.tasks-activity-filters',
|
|
'.gallery-tag-chips',
|
|
'.gallery-album-chips',
|
|
'.gallery-ai-tags',
|
|
'.cal-filters',
|
|
].join(',');
|
|
|
|
function initChipStrip(strip) {
|
|
if (!strip || strip.dataset.chipScrollBound === '1') return;
|
|
const parent = strip.parentElement;
|
|
if (!parent) return;
|
|
|
|
strip.dataset.chipScrollBound = '1';
|
|
strip.classList.add('chip-scroll-strip');
|
|
|
|
const frame = document.createElement('div');
|
|
frame.className = 'doclib-chip-scroll-frame';
|
|
parent.insertBefore(frame, strip);
|
|
frame.appendChild(strip);
|
|
|
|
const makeArrow = (direction, label) => {
|
|
const button = document.createElement('button');
|
|
const glyph = document.createElement('span');
|
|
button.type = 'button';
|
|
button.className = `doclib-chip-scroll-arrow ${direction}`;
|
|
button.setAttribute('aria-label', label);
|
|
button.title = label;
|
|
glyph.className = 'doclib-chip-scroll-arrow-glyph';
|
|
glyph.textContent = direction === 'left' ? '\u2039' : '\u203a';
|
|
button.appendChild(glyph);
|
|
button.addEventListener('click', () => {
|
|
strip.scrollBy({ left: direction === 'left' ? -180 : 180, behavior: 'smooth' });
|
|
});
|
|
frame.appendChild(button);
|
|
return button;
|
|
};
|
|
|
|
const left = makeArrow('left', 'Show previous tags');
|
|
const right = makeArrow('right', 'Show more tags');
|
|
let pointerId = null;
|
|
let startX = 0;
|
|
let startScroll = 0;
|
|
let dragging = false;
|
|
let suppressClick = false;
|
|
|
|
const sync = () => {
|
|
const stripHidden = strip.hidden || getComputedStyle(strip).display === 'none';
|
|
frame.style.display = stripHidden ? 'none' : '';
|
|
if (stripHidden) {
|
|
frame.classList.remove('has-overflow');
|
|
left.hidden = true;
|
|
right.hidden = true;
|
|
return;
|
|
}
|
|
const max = Math.max(0, strip.scrollWidth - strip.clientWidth);
|
|
frame.classList.toggle('has-overflow', max > 1);
|
|
left.hidden = max <= 1 || strip.scrollLeft <= 1;
|
|
right.hidden = max <= 1 || strip.scrollLeft >= max - 1;
|
|
};
|
|
|
|
strip.addEventListener('scroll', sync, { passive: true });
|
|
strip.addEventListener('pointerdown', (event) => {
|
|
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
|
if (event.target.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
|
pointerId = event.pointerId;
|
|
startX = event.clientX;
|
|
startScroll = strip.scrollLeft;
|
|
dragging = false;
|
|
suppressClick = false;
|
|
});
|
|
strip.addEventListener('pointermove', (event) => {
|
|
if (pointerId !== event.pointerId) return;
|
|
const delta = event.clientX - startX;
|
|
if (!dragging && Math.abs(delta) <= 4) return;
|
|
if (!dragging) {
|
|
dragging = true;
|
|
suppressClick = true;
|
|
strip.classList.add('is-pointer-dragging');
|
|
strip.setPointerCapture?.(event.pointerId);
|
|
}
|
|
if (event.cancelable) event.preventDefault();
|
|
strip.scrollLeft = startScroll - delta;
|
|
});
|
|
const stopDragging = (event) => {
|
|
if (pointerId !== event.pointerId) return;
|
|
if (dragging && strip.hasPointerCapture?.(event.pointerId)) {
|
|
strip.releasePointerCapture(event.pointerId);
|
|
}
|
|
pointerId = null;
|
|
dragging = false;
|
|
strip.classList.remove('is-pointer-dragging');
|
|
sync();
|
|
};
|
|
strip.addEventListener('pointerup', stopDragging);
|
|
strip.addEventListener('pointercancel', stopDragging);
|
|
strip.addEventListener('click', (event) => {
|
|
if (!suppressClick) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
suppressClick = false;
|
|
}, true);
|
|
|
|
if (typeof ResizeObserver === 'function') new ResizeObserver(sync).observe(strip);
|
|
new MutationObserver(sync).observe(strip, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['class', 'hidden', 'style'],
|
|
});
|
|
requestAnimationFrame(sync);
|
|
}
|
|
|
|
function scanChipStrips(root = document) {
|
|
if (root.nodeType === Node.ELEMENT_NODE && root.matches?.(CHIP_STRIP_SELECTOR)) initChipStrip(root);
|
|
root.querySelectorAll?.(CHIP_STRIP_SELECTOR).forEach(initChipStrip);
|
|
}
|
|
|
|
export function initChipScrollRows() {
|
|
if (window._chipScrollRowsBound) return;
|
|
window._chipScrollRowsBound = true;
|
|
|
|
['touchstart', 'touchmove'].forEach(type => {
|
|
document.addEventListener(type, event => {
|
|
if (event.target.closest?.('.chip-scroll-strip')) event.stopPropagation();
|
|
}, true);
|
|
});
|
|
|
|
const start = () => {
|
|
scanChipStrips();
|
|
new MutationObserver(records => {
|
|
for (const record of records) {
|
|
record.addedNodes.forEach(node => {
|
|
if (node.nodeType === Node.ELEMENT_NODE) scanChipStrips(node);
|
|
});
|
|
}
|
|
}).observe(document.body, { childList: true, subtree: true });
|
|
};
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
else start();
|
|
}
|
|
|
|
initChipScrollRows();
|