UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

168 lines (167 loc) 6.64 kB
import stopPropagationOfScrollingWheelEvents from '../../../util/stopPropagationOfScrollingWheelEvents.mjs'; import { MutableReactiveValue, ReactiveValue } from '../../../util/ReactiveValue.mjs'; /** * Creates a list that snaps to each item and reports the selected item. */ const makeSnappedList = (itemsValue) => { const container = document.createElement('div'); container.classList.add('toolbar-snapped-scroll-list'); const scroller = document.createElement('div'); scroller.classList.add('scroller'); const visibleIndex = MutableReactiveValue.fromInitialValue(0); let observer = null; const makePageMarkers = () => { const markerContainer = document.createElement('div'); markerContainer.classList.add('page-markers'); // Keyboard focus should go to the main scrolling list. // TODO: Does it make sense for the page marker list to be focusable? markerContainer.setAttribute('tabindex', '-1'); const markers = []; const pairedItems = ReactiveValue.union([ visibleIndex, itemsValue, ]); pairedItems.onUpdateAndNow(([currentVisibleIndex, items]) => { let addedOrRemovedMarkers = false; // Items may have been removed from the list of pages. Make the markers reflect that. while (items.length < markers.length) { markers.pop(); addedOrRemovedMarkers = true; } let activeMarker; for (let i = 0; i < items.length; i++) { let marker; if (i >= markers.length) { marker = document.createElement('div'); // Use a separate content element to increase the clickable size of // the marker. const content = document.createElement('div'); content.classList.add('content'); marker.replaceChildren(content); markers.push(marker); addedOrRemovedMarkers = true; } else { marker = markers[i]; } marker.classList.add('marker'); if (i === currentVisibleIndex) { marker.classList.add('-active'); activeMarker = marker; } else { marker.classList.remove('-active'); } const markerIndex = i; marker.onclick = () => { wrappedItems .get()[markerIndex]?.element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }; } // Only call .replaceChildren when necessary -- doing so on every change would // break transitions. if (addedOrRemovedMarkers) { markerContainer.replaceChildren(...markers); } // Handles the case where there are many markers and the current is offscreen if (activeMarker && markerContainer.scrollHeight > container.clientHeight) { activeMarker.scrollIntoView({ block: 'nearest' }); } if (markers.length === 1) { markerContainer.classList.add('-one-element'); } else { markerContainer.classList.remove('-one-element'); } }); return markerContainer; }; const createObserver = () => { observer = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting && entry.intersectionRatio > 0.7) { const indexString = entry.target.getAttribute('data-item-index'); if (indexString === null) throw new Error('Could not find attribute data-item-index'); const index = Number(indexString); visibleIndex.set(index); break; } } }, { // Element to use as the boudning box with which to intersect. // See https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API root: scroller, // Fraction of an element that must be visible to trigger the callback: threshold: 0.9, }); }; const destroyObserver = () => { if (observer) { observer.disconnect(); visibleIndex.set(0); observer = null; } }; const wrappedItems = ReactiveValue.map(itemsValue, (items) => { return items.map((item, index) => { const wrapper = document.createElement('div'); if (item.element.parentElement) item.element.remove(); wrapper.appendChild(item.element); wrapper.classList.add('item'); wrapper.setAttribute('data-item-index', `${index}`); return { element: wrapper, data: item.data, }; }); }); const lastItems = []; wrappedItems.onUpdateAndNow((items) => { visibleIndex.set(-1); for (const item of lastItems) { observer?.unobserve(item.element); } scroller.replaceChildren(); // An observer is only necessary if there are multiple items to scroll through. if (items.length > 1) { createObserver(); } else { destroyObserver(); } // Different styling is applied when empty if (items.length === 0) { container.classList.add('-empty'); } else { container.classList.remove('-empty'); } for (const item of items) { scroller.appendChild(item.element); } visibleIndex.set(0); if (observer) { for (const item of items) { observer.observe(item.element); } } }); const visibleItem = ReactiveValue.map(visibleIndex, (index) => { const values = itemsValue.get(); if (0 <= index && index < values.length) { return values[index].data; } return null; }); // makeSnappedList is generally shown within the toolbar. This allows users to // scroll it with a touchpad. stopPropagationOfScrollingWheelEvents(scroller); container.replaceChildren(makePageMarkers(), scroller); return { container, visibleItem, }; }; export default makeSnappedList;