08f241d7cccbed09d0b936e9c6f69a1b9a9a0f5a63f7f23bb652c2c84c0d1344371dad81851703b12db537ff827751455d0ce51e592ba280fd33201d7cc4a5 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /* @flow */
  2. import type Router from '../index'
  3. import { assert } from './warn'
  4. import { getStateKey, setStateKey } from './state-key'
  5. import { extend } from './misc'
  6. const positionStore = Object.create(null)
  7. export function setupScroll () {
  8. // Prevent browser scroll behavior on History popstate
  9. if ('scrollRestoration' in window.history) {
  10. window.history.scrollRestoration = 'manual'
  11. }
  12. // Fix for #1585 for Firefox
  13. // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
  14. // Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
  15. // window.location.protocol + '//' + window.location.host
  16. // location.host contains the port and location.hostname doesn't
  17. const protocolAndPath = window.location.protocol + '//' + window.location.host
  18. const absolutePath = window.location.href.replace(protocolAndPath, '')
  19. // preserve existing history state as it could be overriden by the user
  20. const stateCopy = extend({}, window.history.state)
  21. stateCopy.key = getStateKey()
  22. window.history.replaceState(stateCopy, '', absolutePath)
  23. window.addEventListener('popstate', handlePopState)
  24. return () => {
  25. window.removeEventListener('popstate', handlePopState)
  26. }
  27. }
  28. export function handleScroll (
  29. router: Router,
  30. to: Route,
  31. from: Route,
  32. isPop: boolean
  33. ) {
  34. if (!router.app) {
  35. return
  36. }
  37. const behavior = router.options.scrollBehavior
  38. if (!behavior) {
  39. return
  40. }
  41. if (process.env.NODE_ENV !== 'production') {
  42. assert(typeof behavior === 'function', `scrollBehavior must be a function`)
  43. }
  44. // wait until re-render finishes before scrolling
  45. router.app.$nextTick(() => {
  46. const position = getScrollPosition()
  47. const shouldScroll = behavior.call(
  48. router,
  49. to,
  50. from,
  51. isPop ? position : null
  52. )
  53. if (!shouldScroll) {
  54. return
  55. }
  56. if (typeof shouldScroll.then === 'function') {
  57. shouldScroll
  58. .then(shouldScroll => {
  59. scrollToPosition((shouldScroll: any), position)
  60. })
  61. .catch(err => {
  62. if (process.env.NODE_ENV !== 'production') {
  63. assert(false, err.toString())
  64. }
  65. })
  66. } else {
  67. scrollToPosition(shouldScroll, position)
  68. }
  69. })
  70. }
  71. export function saveScrollPosition () {
  72. const key = getStateKey()
  73. if (key) {
  74. positionStore[key] = {
  75. x: window.pageXOffset,
  76. y: window.pageYOffset
  77. }
  78. }
  79. }
  80. function handlePopState (e) {
  81. saveScrollPosition()
  82. if (e.state && e.state.key) {
  83. setStateKey(e.state.key)
  84. }
  85. }
  86. function getScrollPosition (): ?Object {
  87. const key = getStateKey()
  88. if (key) {
  89. return positionStore[key]
  90. }
  91. }
  92. function getElementPosition (el: Element, offset: Object): Object {
  93. const docEl: any = document.documentElement
  94. const docRect = docEl.getBoundingClientRect()
  95. const elRect = el.getBoundingClientRect()
  96. return {
  97. x: elRect.left - docRect.left - offset.x,
  98. y: elRect.top - docRect.top - offset.y
  99. }
  100. }
  101. function isValidPosition (obj: Object): boolean {
  102. return isNumber(obj.x) || isNumber(obj.y)
  103. }
  104. function normalizePosition (obj: Object): Object {
  105. return {
  106. x: isNumber(obj.x) ? obj.x : window.pageXOffset,
  107. y: isNumber(obj.y) ? obj.y : window.pageYOffset
  108. }
  109. }
  110. function normalizeOffset (obj: Object): Object {
  111. return {
  112. x: isNumber(obj.x) ? obj.x : 0,
  113. y: isNumber(obj.y) ? obj.y : 0
  114. }
  115. }
  116. function isNumber (v: any): boolean {
  117. return typeof v === 'number'
  118. }
  119. const hashStartsWithNumberRE = /^#\d/
  120. function scrollToPosition (shouldScroll, position) {
  121. const isObject = typeof shouldScroll === 'object'
  122. if (isObject && typeof shouldScroll.selector === 'string') {
  123. // getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
  124. // but at the same time, it doesn't make much sense to select an element with an id and an extra selector
  125. const el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line
  126. ? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line
  127. : document.querySelector(shouldScroll.selector)
  128. if (el) {
  129. let offset =
  130. shouldScroll.offset && typeof shouldScroll.offset === 'object'
  131. ? shouldScroll.offset
  132. : {}
  133. offset = normalizeOffset(offset)
  134. position = getElementPosition(el, offset)
  135. } else if (isValidPosition(shouldScroll)) {
  136. position = normalizePosition(shouldScroll)
  137. }
  138. } else if (isObject && isValidPosition(shouldScroll)) {
  139. position = normalizePosition(shouldScroll)
  140. }
  141. if (position) {
  142. // $flow-disable-line
  143. if ('scrollBehavior' in document.documentElement.style) {
  144. window.scrollTo({
  145. left: position.x,
  146. top: position.y,
  147. // $flow-disable-line
  148. behavior: shouldScroll.behavior
  149. })
  150. } else {
  151. window.scrollTo(position.x, position.y)
  152. }
  153. }
  154. }