slider.mjs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /* eslint-disable max-statements */
  2. // Composables
  3. import { makeElevationProps } from "../../composables/elevation.mjs";
  4. import { useRtl } from "../../composables/locale.mjs";
  5. import { makeRoundedProps } from "../../composables/rounded.mjs"; // Utilities
  6. import { computed, provide, ref, shallowRef, toRef } from 'vue';
  7. import { clamp, createRange, getDecimals, propsFactory } from "../../util/index.mjs"; // Types
  8. export const VSliderSymbol = Symbol.for('vuetify:v-slider');
  9. export function getOffset(e, el, direction) {
  10. const vertical = direction === 'vertical';
  11. const rect = el.getBoundingClientRect();
  12. const touch = 'touches' in e ? e.touches[0] : e;
  13. return vertical ? touch.clientY - (rect.top + rect.height / 2) : touch.clientX - (rect.left + rect.width / 2);
  14. }
  15. function getPosition(e, position) {
  16. if ('touches' in e && e.touches.length) return e.touches[0][position];else if ('changedTouches' in e && e.changedTouches.length) return e.changedTouches[0][position];else return e[position];
  17. }
  18. export const makeSliderProps = propsFactory({
  19. disabled: {
  20. type: Boolean,
  21. default: null
  22. },
  23. error: Boolean,
  24. readonly: {
  25. type: Boolean,
  26. default: null
  27. },
  28. max: {
  29. type: [Number, String],
  30. default: 100
  31. },
  32. min: {
  33. type: [Number, String],
  34. default: 0
  35. },
  36. step: {
  37. type: [Number, String],
  38. default: 0
  39. },
  40. thumbColor: String,
  41. thumbLabel: {
  42. type: [Boolean, String],
  43. default: undefined,
  44. validator: v => typeof v === 'boolean' || v === 'always'
  45. },
  46. thumbSize: {
  47. type: [Number, String],
  48. default: 20
  49. },
  50. showTicks: {
  51. type: [Boolean, String],
  52. default: false,
  53. validator: v => typeof v === 'boolean' || v === 'always'
  54. },
  55. ticks: {
  56. type: [Array, Object]
  57. },
  58. tickSize: {
  59. type: [Number, String],
  60. default: 2
  61. },
  62. color: String,
  63. trackColor: String,
  64. trackFillColor: String,
  65. trackSize: {
  66. type: [Number, String],
  67. default: 4
  68. },
  69. direction: {
  70. type: String,
  71. default: 'horizontal',
  72. validator: v => ['vertical', 'horizontal'].includes(v)
  73. },
  74. reverse: Boolean,
  75. ...makeRoundedProps(),
  76. ...makeElevationProps({
  77. elevation: 2
  78. }),
  79. ripple: {
  80. type: Boolean,
  81. default: true
  82. }
  83. }, 'Slider');
  84. export const useSteps = props => {
  85. const min = computed(() => parseFloat(props.min));
  86. const max = computed(() => parseFloat(props.max));
  87. const step = computed(() => +props.step > 0 ? parseFloat(props.step) : 0);
  88. const decimals = computed(() => Math.max(getDecimals(step.value), getDecimals(min.value)));
  89. function roundValue(value) {
  90. value = parseFloat(value);
  91. if (step.value <= 0) return value;
  92. const clamped = clamp(value, min.value, max.value);
  93. const offset = min.value % step.value;
  94. const newValue = Math.round((clamped - offset) / step.value) * step.value + offset;
  95. return parseFloat(Math.min(newValue, max.value).toFixed(decimals.value));
  96. }
  97. return {
  98. min,
  99. max,
  100. step,
  101. decimals,
  102. roundValue
  103. };
  104. };
  105. export const useSlider = _ref => {
  106. let {
  107. props,
  108. steps,
  109. onSliderStart,
  110. onSliderMove,
  111. onSliderEnd,
  112. getActiveThumb
  113. } = _ref;
  114. const {
  115. isRtl
  116. } = useRtl();
  117. const isReversed = toRef(props, 'reverse');
  118. const vertical = computed(() => props.direction === 'vertical');
  119. const indexFromEnd = computed(() => vertical.value !== isReversed.value);
  120. const {
  121. min,
  122. max,
  123. step,
  124. decimals,
  125. roundValue
  126. } = steps;
  127. const thumbSize = computed(() => parseInt(props.thumbSize, 10));
  128. const tickSize = computed(() => parseInt(props.tickSize, 10));
  129. const trackSize = computed(() => parseInt(props.trackSize, 10));
  130. const numTicks = computed(() => (max.value - min.value) / step.value);
  131. const disabled = toRef(props, 'disabled');
  132. const thumbColor = computed(() => props.error || props.disabled ? undefined : props.thumbColor ?? props.color);
  133. const trackColor = computed(() => props.error || props.disabled ? undefined : props.trackColor ?? props.color);
  134. const trackFillColor = computed(() => props.error || props.disabled ? undefined : props.trackFillColor ?? props.color);
  135. const mousePressed = shallowRef(false);
  136. const startOffset = shallowRef(0);
  137. const trackContainerRef = ref();
  138. const activeThumbRef = ref();
  139. function parseMouseMove(e) {
  140. const vertical = props.direction === 'vertical';
  141. const start = vertical ? 'top' : 'left';
  142. const length = vertical ? 'height' : 'width';
  143. const position = vertical ? 'clientY' : 'clientX';
  144. const {
  145. [start]: trackStart,
  146. [length]: trackLength
  147. } = trackContainerRef.value?.$el.getBoundingClientRect();
  148. const clickOffset = getPosition(e, position);
  149. // It is possible for left to be NaN, force to number
  150. let clickPos = Math.min(Math.max((clickOffset - trackStart - startOffset.value) / trackLength, 0), 1) || 0;
  151. if (vertical ? indexFromEnd.value : indexFromEnd.value !== isRtl.value) clickPos = 1 - clickPos;
  152. return roundValue(min.value + clickPos * (max.value - min.value));
  153. }
  154. const handleStop = e => {
  155. onSliderEnd({
  156. value: parseMouseMove(e)
  157. });
  158. mousePressed.value = false;
  159. startOffset.value = 0;
  160. };
  161. const handleStart = e => {
  162. activeThumbRef.value = getActiveThumb(e);
  163. if (!activeThumbRef.value) return;
  164. activeThumbRef.value.focus();
  165. mousePressed.value = true;
  166. if (activeThumbRef.value.contains(e.target)) {
  167. startOffset.value = getOffset(e, activeThumbRef.value, props.direction);
  168. } else {
  169. startOffset.value = 0;
  170. onSliderMove({
  171. value: parseMouseMove(e)
  172. });
  173. }
  174. onSliderStart({
  175. value: parseMouseMove(e)
  176. });
  177. };
  178. const moveListenerOptions = {
  179. passive: true,
  180. capture: true
  181. };
  182. function onMouseMove(e) {
  183. onSliderMove({
  184. value: parseMouseMove(e)
  185. });
  186. }
  187. function onSliderMouseUp(e) {
  188. e.stopPropagation();
  189. e.preventDefault();
  190. handleStop(e);
  191. window.removeEventListener('mousemove', onMouseMove, moveListenerOptions);
  192. window.removeEventListener('mouseup', onSliderMouseUp);
  193. }
  194. function onSliderTouchend(e) {
  195. handleStop(e);
  196. window.removeEventListener('touchmove', onMouseMove, moveListenerOptions);
  197. e.target?.removeEventListener('touchend', onSliderTouchend);
  198. }
  199. function onSliderTouchstart(e) {
  200. handleStart(e);
  201. window.addEventListener('touchmove', onMouseMove, moveListenerOptions);
  202. e.target?.addEventListener('touchend', onSliderTouchend, {
  203. passive: false
  204. });
  205. }
  206. function onSliderMousedown(e) {
  207. e.preventDefault();
  208. handleStart(e);
  209. window.addEventListener('mousemove', onMouseMove, moveListenerOptions);
  210. window.addEventListener('mouseup', onSliderMouseUp, {
  211. passive: false
  212. });
  213. }
  214. const position = val => {
  215. const percentage = (val - min.value) / (max.value - min.value) * 100;
  216. return clamp(isNaN(percentage) ? 0 : percentage, 0, 100);
  217. };
  218. const showTicks = toRef(props, 'showTicks');
  219. const parsedTicks = computed(() => {
  220. if (!showTicks.value) return [];
  221. if (!props.ticks) {
  222. return numTicks.value !== Infinity ? createRange(numTicks.value + 1).map(t => {
  223. const value = min.value + t * step.value;
  224. return {
  225. value,
  226. position: position(value)
  227. };
  228. }) : [];
  229. }
  230. if (Array.isArray(props.ticks)) return props.ticks.map(t => ({
  231. value: t,
  232. position: position(t),
  233. label: t.toString()
  234. }));
  235. return Object.keys(props.ticks).map(key => ({
  236. value: parseFloat(key),
  237. position: position(parseFloat(key)),
  238. label: props.ticks[key]
  239. }));
  240. });
  241. const hasLabels = computed(() => parsedTicks.value.some(_ref2 => {
  242. let {
  243. label
  244. } = _ref2;
  245. return !!label;
  246. }));
  247. const data = {
  248. activeThumbRef,
  249. color: toRef(props, 'color'),
  250. decimals,
  251. disabled,
  252. direction: toRef(props, 'direction'),
  253. elevation: toRef(props, 'elevation'),
  254. hasLabels,
  255. isReversed,
  256. indexFromEnd,
  257. min,
  258. max,
  259. mousePressed,
  260. numTicks,
  261. onSliderMousedown,
  262. onSliderTouchstart,
  263. parsedTicks,
  264. parseMouseMove,
  265. position,
  266. readonly: toRef(props, 'readonly'),
  267. rounded: toRef(props, 'rounded'),
  268. roundValue,
  269. showTicks,
  270. startOffset,
  271. step,
  272. thumbSize,
  273. thumbColor,
  274. thumbLabel: toRef(props, 'thumbLabel'),
  275. ticks: toRef(props, 'ticks'),
  276. tickSize,
  277. trackColor,
  278. trackContainerRef,
  279. trackFillColor,
  280. trackSize,
  281. vertical
  282. };
  283. provide(VSliderSymbol, data);
  284. return data;
  285. };
  286. //# sourceMappingURL=slider.mjs.map