UNPKG

@helpwave/hightide

Version:

helpwave's component and theming library

1 lines 18 kB
{"version":3,"sources":["../../../src/components/user-action/ScrollPicker.tsx","../../../src/util/noop.ts","../../../src/util/array.ts","../../../src/util/math.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react'\nimport clsx from 'clsx'\nimport { noop } from '../../util/noop'\nimport { getNeighbours, range } from '../../util/array'\nimport { clamp } from '../../util/math'\n\nexport type ScrollPickerProps<T> = {\n options: T[],\n mapping: (value: T) => string,\n selected?: T,\n onChange?: (value: T) => void,\n disabled?: boolean,\n}\n\ntype AnimationData<T> = {\n /** The index we scroll to */\n targetIndex: number,\n /** The index we are currently showing centered */\n currentIndex: number,\n items: T[],\n /** From -0.5 to 0.5 */\n transition: number,\n velocity: number,\n animationVelocity: number,\n lastTimeStamp?: number,\n lastScrollTimeStamp?: number,\n}\n\nconst up = 1\nconst down = -1\ntype Direction = 1 | -1\n\n/**\n * A component for picking an option by scrolling\n */\nexport const ScrollPicker = <T, >({\n options,\n mapping,\n selected,\n onChange = noop,\n disabled = false,\n }: ScrollPickerProps<T>) => {\n let selectedIndex = 0\n if (selected && options.indexOf(selected) !== -1) {\n selectedIndex = options.indexOf(selected)\n }\n const [{\n currentIndex,\n transition,\n items,\n lastTimeStamp\n }, setAnimation] = useState<AnimationData<T>>({\n targetIndex: selectedIndex,\n currentIndex: disabled ? selectedIndex : 0,\n velocity: 0,\n animationVelocity: Math.floor(options.length / 2),\n transition: 0,\n items: options,\n })\n\n const itemsShownCount = 5\n const shownItems = getNeighbours(range(items.length), currentIndex).map(index => ({\n name: mapping(items[index]!), index\n }))\n\n const itemHeight = 40\n const distance = 8\n\n const containerHeight = itemHeight * (itemsShownCount - 2) + distance * (itemsShownCount - 2 + 1)\n\n const getDirection = useCallback((targetIndex: number, currentIndex: number, transition: number, length: number): Direction => {\n if (targetIndex === currentIndex) {\n return transition > 0 ? up : down\n }\n let distanceForward = targetIndex - currentIndex\n if (distanceForward < 0) {\n distanceForward += length\n }\n return distanceForward >= length / 2 ? down : up\n }, [])\n\n const animate = useCallback((timestamp: number, startTime: number | undefined) => {\n setAnimation((prevState) => {\n const {\n targetIndex,\n currentIndex,\n transition,\n animationVelocity,\n velocity,\n items,\n lastScrollTimeStamp\n } = prevState\n if (disabled) {\n return { ...prevState, currentIndex: targetIndex, velocity: 0, lastTimeStamp: timestamp }\n }\n if ((targetIndex === currentIndex && velocity === 0 && transition === 0) || !startTime) {\n return { ...prevState, lastTimeStamp: timestamp }\n }\n const progress = (timestamp - startTime) / 1000 // to seconds\n const direction = getDirection(targetIndex, currentIndex, transition, items.length)\n\n let newVelocity = velocity\n let usedVelocity\n let newCurrentIndex = currentIndex\n const isAutoScrolling = velocity === 0 && (!lastScrollTimeStamp || timestamp - lastScrollTimeStamp > 300)\n\n const newLastScrollTimeStamp = velocity !== 0 ? timestamp : lastScrollTimeStamp\n\n // manual scrolling\n if (isAutoScrolling) {\n usedVelocity = direction * animationVelocity\n } else {\n usedVelocity = velocity\n newVelocity = velocity * 0.5 // drag loss\n if (Math.abs(newVelocity) <= 0.05) {\n newVelocity = 0\n }\n }\n\n let newTransition = transition + usedVelocity * progress\n const changeThreshold = 0.5\n\n while (newTransition >= changeThreshold) {\n if (newCurrentIndex === targetIndex && newTransition >= changeThreshold && isAutoScrolling) {\n newTransition = 0\n break\n }\n newCurrentIndex = (currentIndex + 1) % items.length\n newTransition -= 1\n }\n if (newTransition >= changeThreshold) {\n newTransition = 0\n }\n while (newTransition <= -changeThreshold) {\n if (newCurrentIndex === targetIndex && newTransition <= -changeThreshold && isAutoScrolling) {\n newTransition = 0\n break\n }\n newCurrentIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1\n newTransition += 1\n }\n let newTargetIndex = targetIndex\n if (!isAutoScrolling) {\n newTargetIndex = newCurrentIndex\n }\n\n if ((currentIndex !== newTargetIndex || newTargetIndex !== targetIndex) && newTargetIndex === newCurrentIndex) {\n onChange(items[newCurrentIndex]!)\n }\n return {\n targetIndex: newTargetIndex,\n currentIndex: newCurrentIndex,\n animationVelocity,\n transition: newTransition,\n velocity: newVelocity,\n items,\n lastTimeStamp: timestamp,\n lastScrollTimeStamp: newLastScrollTimeStamp\n }\n })\n }, [disabled, getDirection, onChange])\n\n useEffect(() => {\n // constant update\n requestAnimationFrame((timestamp) => animate(timestamp, lastTimeStamp))\n })\n\n const opacity = (transition: number, index: number, itemsCount: number) => {\n const max = 100\n const min = 0\n const distance = max - min\n\n let opacityValue = min\n const unitTransition = clamp((transition) / 0.5)\n if (index === 1 || index === itemsCount - 2) {\n if (index === 1 && transition > 0) {\n opacityValue += Math.floor(unitTransition * distance)\n }\n if (index === itemsCount - 2 && transition < 0) {\n opacityValue += Math.floor(unitTransition * distance)\n }\n } else {\n opacityValue = max\n }\n\n // TODO this is not the right value for the bottom entry\n return clamp(1 - (opacityValue / max))\n }\n\n return (\n <div\n className=\"relative overflow-hidden\"\n style={{ height: containerHeight }}\n onWheel={event => {\n if (event.deltaY !== 0) {\n // TODO slower increase\n setAnimation(({ velocity, ...animationData }) =>\n ({ ...animationData, velocity: velocity + event.deltaY }))\n }\n }}\n >\n <div className=\"absolute top-1/2 -translate-y-1/2 -translate-x-1/2 left-1/2\">\n <div\n className=\"absolute z-[1] top-1/2 -translate-y-1/2 -translate-x-1/2 left-1/2 w-full min-w-[40px] border border-divider/50 border-y-2 border-x-0 \"\n style={{ height: `${itemHeight}px` }}\n />\n <div\n className=\"flex-col-2 select-none\"\n style={{\n transform: `translateY(${-transition * (distance + itemHeight)}px)`,\n columnGap: `${distance}px`,\n }}\n >\n {shownItems.map(({ name, index }, arrayIndex) => (\n <div\n key={index}\n className={clsx(\n `flex-col-2 items-center justify-center rounded-md`,\n {\n 'text-primary font-bold': currentIndex === index,\n 'text-on-background': currentIndex === index,\n 'cursor-pointer': !disabled,\n 'cursor-not-allowed': disabled,\n }\n )}\n style={{\n opacity: currentIndex !== index ? opacity(transition, arrayIndex, shownItems.length) : undefined,\n height: `${itemHeight}px`,\n maxHeight: `${itemHeight}px`,\n }}\n onClick={() => !disabled && setAnimation(prevState => ({ ...prevState, targetIndex: index }))}\n >\n {name}\n </div>\n ))}\n </div>\n </div>\n </div>\n )\n}\n","export const noop = () => undefined\n","export const equalSizeGroups = <T>(array: T[], groupSize: number): T[][] => {\n if (groupSize <= 0) {\n console.warn(`group size should be greater than 0: groupSize = ${groupSize}`)\n return [[...array]]\n }\n\n const groups = []\n for (let i = 0; i < array.length; i += groupSize) {\n groups.push(array.slice(i, Math.min(i + groupSize, array.length)))\n }\n return groups\n}\n\nexport type RangeOptions = {\n /** Whether the range can be defined empty via end < start without a warning */\n allowEmptyRange: boolean,\n stepSize: number,\n exclusiveStart: boolean,\n exclusiveEnd: boolean,\n}\n\nconst defaultRangeOptions: RangeOptions = {\n allowEmptyRange: false,\n stepSize: 1,\n exclusiveStart: false,\n exclusiveEnd: true,\n}\n\n/**\n * @param endOrRange The end value or a range [start, end], end is exclusive\n * @param options the options for defining the range\n */\nexport const range = (endOrRange: number | [number, number], options?: Partial<RangeOptions>): number[] => {\n const { allowEmptyRange, stepSize, exclusiveStart, exclusiveEnd } = { ...defaultRangeOptions, ...options }\n let start = 0\n let end: number\n if (typeof endOrRange === 'number') {\n end = endOrRange\n } else {\n start = endOrRange[0]\n end = endOrRange[1]\n }\n if (!exclusiveEnd) {\n end -= 1\n }\n if (exclusiveStart) {\n start += 1\n }\n\n if (end - 1 < start) {\n if (!allowEmptyRange) {\n console.warn(`range: end (${end}) < start (${start}) should be allowed explicitly, set options.allowEmptyRange to true`)\n }\n return []\n }\n return Array.from({ length: end - start }, (_, index) => index * stepSize + start)\n}\n\n/** Finds the closest match\n * @param list The list of all possible matches\n * @param firstCloser Return whether item1 is closer than item2\n */\nexport const closestMatch = <T>(list: T[], firstCloser: (item1: T, item2: T) => boolean) => {\n return list.reduce((item1, item2) => {\n return firstCloser(item1, item2) ? item1 : item2\n })\n}\n\n/**\n * returns the item in middle of a list and its neighbours before and after\n * e.g. [1,2,3,4,5,6] for item = 1 would return [5,6,1,2,3]\n */\nexport const getNeighbours = <T>(list: T[], item: T, neighbourDistance: number = 2) => {\n const index = list.indexOf(item)\n const totalItems = neighbourDistance * 2 + 1\n if (list.length < totalItems) {\n console.warn('List is to short')\n return list\n }\n\n if (index === -1) {\n console.error('item not found in list')\n return list.splice(0, totalItems)\n }\n\n let start = index - neighbourDistance\n if (start < 0) {\n start += list.length\n }\n const end = (index + neighbourDistance + 1) % list.length\n\n const result: T[] = []\n let ignoreOnce = list.length === totalItems\n for (let i = start; i !== end || ignoreOnce; i = (i + 1) % list.length) {\n result.push(list[i]!)\n if (end === i && ignoreOnce) {\n ignoreOnce = false\n }\n }\n return result\n}\n\nexport const createLoopingListWithIndex = <T>(list: T[], startIndex: number = 0, length: number = 0, forwards: boolean = true) => {\n if (length < 0) {\n console.warn(`createLoopingList: length must be >= 0, given ${length}`)\n } else if (length === 0) {\n length = list.length\n }\n\n const returnList: [number, T][] = []\n\n if (forwards) {\n for (let i = startIndex; returnList.length < length; i = (i + 1) % list.length) {\n returnList.push([i, list[i]!])\n }\n } else {\n for (let i = startIndex; returnList.length < length; i = i === 0 ? i = list.length - 1 : i - 1) {\n returnList.push([i, list[i]!])\n }\n }\n\n return returnList\n}\n\nexport const createLoopingList = <T>(list: T[], startIndex: number = 0, length: number = 0, forwards: boolean = true) => {\n return createLoopingListWithIndex(list, startIndex, length, forwards).map(([_, item]) => item)\n}\n\nexport const ArrayUtil = {\n unique: <T>(list: T[]): T[] => {\n const seen = new Set<T>()\n return list.filter((item) => {\n if (seen.has(item)) {\n return false\n }\n seen.add(item)\n return true\n })\n },\n\n difference: <T>(list: T[], removeList: T[]): T[] => {\n const remove = new Set<T>(removeList)\n return list.filter((item) => !remove.has(item))\n }\n}\n","export const clamp = (value: number, min: number = 0, max: number = 1): number => {\n return Math.min(Math.max(value, min), max)\n}\n"],"mappings":"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","names":["currentIndex","transition","items","distance"]}