scrollStrategies.mjs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. // Utilities
  2. import { effectScope, onScopeDispose, watchEffect } from 'vue';
  3. import { requestNewFrame } from "./requestNewFrame.mjs";
  4. import { convertToUnit, getScrollParents, hasScrollbar, IN_BROWSER, propsFactory } from "../../util/index.mjs"; // Types
  5. const scrollStrategies = {
  6. none: null,
  7. close: closeScrollStrategy,
  8. block: blockScrollStrategy,
  9. reposition: repositionScrollStrategy
  10. };
  11. export const makeScrollStrategyProps = propsFactory({
  12. scrollStrategy: {
  13. type: [String, Function],
  14. default: 'block',
  15. validator: val => typeof val === 'function' || val in scrollStrategies
  16. }
  17. }, 'VOverlay-scroll-strategies');
  18. export function useScrollStrategies(props, data) {
  19. if (!IN_BROWSER) return;
  20. let scope;
  21. watchEffect(async () => {
  22. scope?.stop();
  23. if (!(data.isActive.value && props.scrollStrategy)) return;
  24. scope = effectScope();
  25. await new Promise(resolve => setTimeout(resolve));
  26. scope.active && scope.run(() => {
  27. if (typeof props.scrollStrategy === 'function') {
  28. props.scrollStrategy(data, props, scope);
  29. } else {
  30. scrollStrategies[props.scrollStrategy]?.(data, props, scope);
  31. }
  32. });
  33. });
  34. onScopeDispose(() => {
  35. scope?.stop();
  36. });
  37. }
  38. function closeScrollStrategy(data) {
  39. function onScroll(e) {
  40. data.isActive.value = false;
  41. }
  42. bindScroll(data.targetEl.value ?? data.contentEl.value, onScroll);
  43. }
  44. function blockScrollStrategy(data, props) {
  45. const offsetParent = data.root.value?.offsetParent;
  46. const scrollElements = [...new Set([...getScrollParents(data.targetEl.value, props.contained ? offsetParent : undefined), ...getScrollParents(data.contentEl.value, props.contained ? offsetParent : undefined)])].filter(el => !el.classList.contains('v-overlay-scroll-blocked'));
  47. const scrollbarWidth = window.innerWidth - document.documentElement.offsetWidth;
  48. const scrollableParent = (el => hasScrollbar(el) && el)(offsetParent || document.documentElement);
  49. if (scrollableParent) {
  50. data.root.value.classList.add('v-overlay--scroll-blocked');
  51. }
  52. scrollElements.forEach((el, i) => {
  53. el.style.setProperty('--v-body-scroll-x', convertToUnit(-el.scrollLeft));
  54. el.style.setProperty('--v-body-scroll-y', convertToUnit(-el.scrollTop));
  55. if (el !== document.documentElement) {
  56. el.style.setProperty('--v-scrollbar-offset', convertToUnit(scrollbarWidth));
  57. }
  58. el.classList.add('v-overlay-scroll-blocked');
  59. });
  60. onScopeDispose(() => {
  61. scrollElements.forEach((el, i) => {
  62. const x = parseFloat(el.style.getPropertyValue('--v-body-scroll-x'));
  63. const y = parseFloat(el.style.getPropertyValue('--v-body-scroll-y'));
  64. const scrollBehavior = el.style.scrollBehavior;
  65. el.style.scrollBehavior = 'auto';
  66. el.style.removeProperty('--v-body-scroll-x');
  67. el.style.removeProperty('--v-body-scroll-y');
  68. el.style.removeProperty('--v-scrollbar-offset');
  69. el.classList.remove('v-overlay-scroll-blocked');
  70. el.scrollLeft = -x;
  71. el.scrollTop = -y;
  72. el.style.scrollBehavior = scrollBehavior;
  73. });
  74. if (scrollableParent) {
  75. data.root.value.classList.remove('v-overlay--scroll-blocked');
  76. }
  77. });
  78. }
  79. function repositionScrollStrategy(data, props, scope) {
  80. let slow = false;
  81. let raf = -1;
  82. let ric = -1;
  83. function update(e) {
  84. requestNewFrame(() => {
  85. const start = performance.now();
  86. data.updateLocation.value?.(e);
  87. const time = performance.now() - start;
  88. slow = time / (1000 / 60) > 2;
  89. });
  90. }
  91. ric = (typeof requestIdleCallback === 'undefined' ? cb => cb() : requestIdleCallback)(() => {
  92. scope.run(() => {
  93. bindScroll(data.targetEl.value ?? data.contentEl.value, e => {
  94. if (slow) {
  95. // If the position calculation is slow,
  96. // defer updates until scrolling is finished.
  97. // Browsers usually fire one scroll event per frame so
  98. // we just wait until we've got two frames without an event
  99. cancelAnimationFrame(raf);
  100. raf = requestAnimationFrame(() => {
  101. raf = requestAnimationFrame(() => {
  102. update(e);
  103. });
  104. });
  105. } else {
  106. update(e);
  107. }
  108. });
  109. });
  110. });
  111. onScopeDispose(() => {
  112. typeof cancelIdleCallback !== 'undefined' && cancelIdleCallback(ric);
  113. cancelAnimationFrame(raf);
  114. });
  115. }
  116. /** @private */
  117. function bindScroll(el, onScroll) {
  118. const scrollElements = [document, ...getScrollParents(el)];
  119. scrollElements.forEach(el => {
  120. el.addEventListener('scroll', onScroll, {
  121. passive: true
  122. });
  123. });
  124. onScopeDispose(() => {
  125. scrollElements.forEach(el => {
  126. el.removeEventListener('scroll', onScroll);
  127. });
  128. });
  129. }
  130. //# sourceMappingURL=scrollStrategies.mjs.map