diff --git a/app/soapbox/components/ui/index.ts b/app/soapbox/components/ui/index.ts index 0dd6fff52..c3f148833 100644 --- a/app/soapbox/components/ui/index.ts +++ b/app/soapbox/components/ui/index.ts @@ -41,6 +41,7 @@ export { default as PhoneInput } from './phone-input/phone-input'; export { default as ProgressBar } from './progress-bar/progress-bar'; export { default as RadioButton } from './radio-button/radio-button'; export { default as Select } from './select/select'; +export { default as Slider } from './slider/slider'; export { default as Spinner } from './spinner/spinner'; export { default as Stack } from './stack/stack'; export { default as Streamfield } from './streamfield/streamfield'; diff --git a/app/soapbox/components/ui/slider/slider.tsx b/app/soapbox/components/ui/slider/slider.tsx new file mode 100644 index 000000000..65cf94a9f --- /dev/null +++ b/app/soapbox/components/ui/slider/slider.tsx @@ -0,0 +1,124 @@ +import throttle from 'lodash/throttle'; +import React, { useRef } from 'react'; + +type Point = { x: number, y: number }; + +interface ISlider { + /** Value between 0 and 1. */ + value: number + /** Callback when the value changes. */ + onChange(value: number): void +} + +/** Draggable slider component. */ +const Slider: React.FC = ({ value, onChange }) => { + const node = useRef(null); + + const handleMouseDown: React.MouseEventHandler = e => { + document.addEventListener('mousemove', handleMouseSlide, true); + document.addEventListener('mouseup', handleMouseUp, true); + document.addEventListener('touchmove', handleMouseSlide, true); + document.addEventListener('touchend', handleMouseUp, true); + + handleMouseSlide(e); + + e.preventDefault(); + e.stopPropagation(); + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseSlide, true); + document.removeEventListener('mouseup', handleMouseUp, true); + document.removeEventListener('touchmove', handleMouseSlide, true); + document.removeEventListener('touchend', handleMouseUp, true); + }; + + const handleMouseSlide = throttle(e => { + if (node.current) { + const { x } = getPointerPosition(node.current, e); + + if (!isNaN(x)) { + let slideamt = x; + + if (x > 1) { + slideamt = 1; + } else if (x < 0) { + slideamt = 0; + } + + onChange(slideamt); + } + } + }, 60); + + return ( +
+
+
+ +
+ ); +}; + +const findElementPosition = (el: HTMLElement) => { + let box; + + if (el.getBoundingClientRect && el.parentNode) { + box = el.getBoundingClientRect(); + } + + if (!box) { + return { + left: 0, + top: 0, + }; + } + + const docEl = document.documentElement; + const body = document.body; + + const clientLeft = docEl.clientLeft || body.clientLeft || 0; + const scrollLeft = window.pageXOffset || body.scrollLeft; + const left = (box.left + scrollLeft) - clientLeft; + + const clientTop = docEl.clientTop || body.clientTop || 0; + const scrollTop = window.pageYOffset || body.scrollTop; + const top = (box.top + scrollTop) - clientTop; + + return { + left: Math.round(left), + top: Math.round(top), + }; +}; + + +const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => { + const box = findElementPosition(el); + const boxW = el.offsetWidth; + const boxH = el.offsetHeight; + const boxY = box.top; + const boxX = box.left; + + let pageY = event.pageY; + let pageX = event.pageX; + + if (event.changedTouches) { + pageX = event.changedTouches[0].pageX; + pageY = event.changedTouches[0].pageY; + } + + return { + y: Math.max(0, Math.min(1, (pageY - boxY) / boxH)), + x: Math.max(0, Math.min(1, (pageX - boxX) / boxW)), + }; +}; + +export default Slider; \ No newline at end of file