goto.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. // Utilities
  2. import { computed, inject } from 'vue';
  3. import { useRtl } from "./locale.mjs";
  4. import { clamp, consoleWarn, mergeDeep, refElement } from "../util/index.mjs"; // Types
  5. export const GoToSymbol = Symbol.for('vuetify:goto');
  6. function genDefaults() {
  7. return {
  8. container: undefined,
  9. duration: 300,
  10. layout: false,
  11. offset: 0,
  12. easing: 'easeInOutCubic',
  13. patterns: {
  14. linear: t => t,
  15. easeInQuad: t => t ** 2,
  16. easeOutQuad: t => t * (2 - t),
  17. easeInOutQuad: t => t < 0.5 ? 2 * t ** 2 : -1 + (4 - 2 * t) * t,
  18. easeInCubic: t => t ** 3,
  19. easeOutCubic: t => --t ** 3 + 1,
  20. easeInOutCubic: t => t < 0.5 ? 4 * t ** 3 : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
  21. easeInQuart: t => t ** 4,
  22. easeOutQuart: t => 1 - --t ** 4,
  23. easeInOutQuart: t => t < 0.5 ? 8 * t ** 4 : 1 - 8 * --t ** 4,
  24. easeInQuint: t => t ** 5,
  25. easeOutQuint: t => 1 + --t ** 5,
  26. easeInOutQuint: t => t < 0.5 ? 16 * t ** 5 : 1 + 16 * --t ** 5
  27. }
  28. };
  29. }
  30. function getContainer(el) {
  31. return getTarget(el) ?? (document.scrollingElement || document.body);
  32. }
  33. function getTarget(el) {
  34. return typeof el === 'string' ? document.querySelector(el) : refElement(el);
  35. }
  36. function getOffset(target, horizontal, rtl) {
  37. if (typeof target === 'number') return horizontal && rtl ? -target : target;
  38. let el = getTarget(target);
  39. let totalOffset = 0;
  40. while (el) {
  41. totalOffset += horizontal ? el.offsetLeft : el.offsetTop;
  42. el = el.offsetParent;
  43. }
  44. return totalOffset;
  45. }
  46. export function createGoTo(options, locale) {
  47. return {
  48. rtl: locale.isRtl,
  49. options: mergeDeep(genDefaults(), options)
  50. };
  51. }
  52. export async function scrollTo(_target, _options, horizontal, goTo) {
  53. const property = horizontal ? 'scrollLeft' : 'scrollTop';
  54. const options = mergeDeep(goTo?.options ?? genDefaults(), _options);
  55. const rtl = goTo?.rtl.value;
  56. const target = (typeof _target === 'number' ? _target : getTarget(_target)) ?? 0;
  57. const container = options.container === 'parent' && target instanceof HTMLElement ? target.parentElement : getContainer(options.container);
  58. const ease = typeof options.easing === 'function' ? options.easing : options.patterns[options.easing];
  59. if (!ease) throw new TypeError(`Easing function "${options.easing}" not found.`);
  60. let targetLocation;
  61. if (typeof target === 'number') {
  62. targetLocation = getOffset(target, horizontal, rtl);
  63. } else {
  64. targetLocation = getOffset(target, horizontal, rtl) - getOffset(container, horizontal, rtl);
  65. if (options.layout) {
  66. const styles = window.getComputedStyle(target);
  67. const layoutOffset = styles.getPropertyValue('--v-layout-top');
  68. if (layoutOffset) targetLocation -= parseInt(layoutOffset, 10);
  69. }
  70. }
  71. targetLocation += options.offset;
  72. targetLocation = clampTarget(container, targetLocation, !!rtl, !!horizontal);
  73. const startLocation = container[property] ?? 0;
  74. if (targetLocation === startLocation) return Promise.resolve(targetLocation);
  75. const startTime = performance.now();
  76. return new Promise(resolve => requestAnimationFrame(function step(currentTime) {
  77. const timeElapsed = currentTime - startTime;
  78. const progress = timeElapsed / options.duration;
  79. const location = Math.floor(startLocation + (targetLocation - startLocation) * ease(clamp(progress, 0, 1)));
  80. container[property] = location;
  81. // Allow for some jitter if target time has elapsed
  82. if (progress >= 1 && Math.abs(location - container[property]) < 10) {
  83. return resolve(targetLocation);
  84. } else if (progress > 2) {
  85. // The target might not be reachable
  86. consoleWarn('Scroll target is not reachable');
  87. return resolve(container[property]);
  88. }
  89. requestAnimationFrame(step);
  90. }));
  91. }
  92. export function useGoTo() {
  93. let _options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  94. const goToInstance = inject(GoToSymbol);
  95. const {
  96. isRtl
  97. } = useRtl();
  98. if (!goToInstance) throw new Error('[Vuetify] Could not find injected goto instance');
  99. const goTo = {
  100. ...goToInstance,
  101. // can be set via VLocaleProvider
  102. rtl: computed(() => goToInstance.rtl.value || isRtl.value)
  103. };
  104. async function go(target, options) {
  105. return scrollTo(target, mergeDeep(_options, options), false, goTo);
  106. }
  107. go.horizontal = async (target, options) => {
  108. return scrollTo(target, mergeDeep(_options, options), true, goTo);
  109. };
  110. return go;
  111. }
  112. /**
  113. * Clamp target value to achieve a smooth scroll animation
  114. * when the value goes outside the scroll container size
  115. */
  116. function clampTarget(container, value, rtl, horizontal) {
  117. const {
  118. scrollWidth,
  119. scrollHeight
  120. } = container;
  121. const [containerWidth, containerHeight] = container === document.scrollingElement ? [window.innerWidth, window.innerHeight] : [container.offsetWidth, container.offsetHeight];
  122. let min;
  123. let max;
  124. if (horizontal) {
  125. if (rtl) {
  126. min = -(scrollWidth - containerWidth);
  127. max = 0;
  128. } else {
  129. min = 0;
  130. max = scrollWidth - containerWidth;
  131. }
  132. } else {
  133. min = 0;
  134. max = scrollHeight + -containerHeight;
  135. }
  136. return Math.max(Math.min(value, max), min);
  137. }
  138. //# sourceMappingURL=goto.mjs.map