Files
odysseus/static/js/chipScroll.js
T

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();