]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(scroll): add scrollBehavior for html5 history
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 5 Aug 2019 18:45:54 +0000 (20:45 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 5 Aug 2019 18:45:54 +0000 (20:45 +0200)
explorations/html5.html
explorations/html5.ts
src/history/html5.ts
src/router.ts
src/utils/scroll.ts [new file with mode: 0644]

index 00b19163f83f6aeefdbedcdad6b1bb58df41cbae..9aba461c0eebf672e40fa0d472220537f6379d5d 100644 (file)
@@ -6,6 +6,35 @@
     <meta http-equiv="X-UA-Compatible" content="ie=edge" />
     <title>Testing History HTML5</title>
     <script src="https://polyfill.io/v3/polyfill.min.js?features=default%2Ces2015"></script>
+
+    <style>
+      .long {
+        background-color: lightgray;
+        height: 3000px;
+      }
+      .fade-enter-active,
+      .fade-leave-active {
+        transition: opacity 0.15s ease;
+      }
+      .fade-enter,
+      .fade-leave-active {
+        opacity: 0;
+      }
+      .child-view {
+        position: absolute;
+        transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
+      }
+      .slide-left-enter,
+      .slide-right-leave-active {
+        opacity: 0;
+        transform: translate(30px, 0);
+      }
+      .slide-left-leave-active,
+      .slide-right-enter {
+        opacity: 0;
+        transform: translate(-30px, 0);
+      }
+    </style>
   </head>
   <body>
     <div id="app">
@@ -18,6 +47,9 @@
         <li>
           <router-link to="/">/</router-link>
         </li>
+        <li>
+          <router-link to="/long-0">/long-0</router-link>
+        </li>
         <li>
           <router-link to="/users/5">/users/5</router-link>
         </li>
           <router-link :to="{ name: 'docs' }">Doc with same id</router-link>
         </li> -->
       </ul>
-      <router-view></router-view>
+      <transition
+        name="fade"
+        mode="out-in"
+        @after-enter="flushWaiter"
+        @before-leave="setupWaiter"
+      >
+        <router-view></router-view>
+      </transition>
     </div>
   </body>
 </html>
index 19128c0cdddf0d43593366e275edebabc4f979f5..f872c78abd710796ec568c3e3235188839295e62 100644 (file)
@@ -37,6 +37,19 @@ const User: RouteComponent = {
   template: `<div>User: {{ $route.params.id }}</div>`,
 }
 
+const LongView: RouteComponent = {
+  template: `
+  <section>
+    <div class="long">This one is long: {{ $route.params.n }}. Go down to click on a link</div>
+    <p class="long">
+      <router-link
+        :to="{ name: 'long', params: { n: Number($route.params.n || 0) + 1 }}"
+        >/long-{{ Number($route.params.n || 0) + 1 }}</router-link>
+    </p>
+  </section>
+  `,
+}
+
 const GuardedWithLeave: RouteComponent = {
   template: `<div>
     <p>try to leave</p>
@@ -47,8 +60,35 @@ const GuardedWithLeave: RouteComponent = {
   },
 }
 
-// const hist = new HTML5History()
-const hist = new HashHistory()
+if ('scrollRestoration' in history) {
+  history.scrollRestoration = 'manual'
+}
+
+class ScrollQueue {
+  private resolve: (() => void) | null = null
+  private promise: Promise<any> | null = null
+
+  add() {
+    this.promise = new Promise(resolve => {
+      this.resolve = resolve
+    })
+  }
+
+  flush() {
+    this.resolve && this.resolve()
+    this.resolve = null
+    this.promise = null
+  }
+
+  async wait() {
+    await this.promise
+  }
+}
+
+const scrollWaiter = new ScrollQueue()
+
+const hist = new HTML5History()
+// const hist = new HashHistory()
 const router = new Router({
   history: hist,
   routes: [
@@ -57,6 +97,7 @@ const router = new Router({
     { path: '/documents/:id', name: 'docs', component: User },
     { path: '/n/:n', name: 'increment', component },
     { path: '/multiple/:a/:b', name: 'multiple', component },
+    { path: '/long-:n', name: 'long', component: LongView },
     {
       path: '/with-guard/:n',
       name: 'guarded',
@@ -78,6 +119,14 @@ const router = new Router({
     },
     // { path: /^\/about\/?$/, component },
   ],
+  async scrollBehavior(to, from, savedPosition) {
+    await scrollWaiter.wait()
+    if (savedPosition) {
+      return savedPosition
+    } else {
+      return { x: 0, y: 0 }
+    }
+  },
 })
 
 // for testing purposes
@@ -173,6 +222,15 @@ window.vm = new Vue({
     shared,
   },
 
+  methods: {
+    flushWaiter() {
+      scrollWaiter.flush()
+    },
+    setupWaiter() {
+      scrollWaiter.add()
+    },
+  },
+
   // try out watchers
   // watch: {
   //   '$route.params.id' (id) {
index 0fb5a702cdbc13d33e10037371e7db0ff53a24d9..5dc32b010a91399c337b0d3acf8d9ed7f059b5ab 100644 (file)
@@ -1,6 +1,7 @@
 import consola from '../consola'
 import { BaseHistory, HistoryLocationNormalized, HistoryLocation } from './base'
 import { NavigationCallback, HistoryState, NavigationDirection } from './base'
+import { computeScrollPosition, ScrollToPosition } from '../utils/scroll'
 
 const cs = consola.withTag('html5')
 
@@ -16,6 +17,7 @@ interface StateEntry {
   current: HistoryLocationNormalized
   forward: HistoryLocationNormalized | null
   replaced: boolean
+  scroll: ScrollToPosition
 }
 
 // TODO: pretty useless right now except for typing
@@ -30,6 +32,7 @@ function buildState(
     current,
     forward,
     replaced,
+    scroll: computeScrollPosition(),
   }
 }
 
index f6c69ba5290fb4c9e59726e4c991cc036971fc64..a9f383ea91208b3e827be59d193c7d095b79ed04 100644 (file)
@@ -17,6 +17,11 @@ import {
   MatcherLocation,
   RouteQueryAndHash,
 } from './types/index'
+import {
+  ScrollToPosition,
+  ScrollPosition,
+  scrollToPosition,
+} from './utils/scroll'
 
 import { guardToPromiseFn, extractComponentsGuards } from './utils'
 import {
@@ -25,9 +30,19 @@ import {
   NavigationCancelled,
 } from './errors'
 
+interface ScrollBehavior {
+  (
+    to: RouteLocationNormalized,
+    from: RouteLocationNormalized,
+    savedPosition: ScrollToPosition
+  ): ScrollPosition | Promise<ScrollPosition>
+}
+
 export interface RouterOptions {
   history: BaseHistory
   routes: RouteRecord[]
+  // TODO: async version
+  scrollBehavior?: ScrollBehavior
 }
 
 type ErrorHandler = (error: any) => any
@@ -46,10 +61,12 @@ export class Router {
   private errorHandlers: ErrorHandler[] = []
   private ready: boolean = false
   private onReadyCbs: OnReadyCallback[] = []
+  private scrollBehavior?: ScrollBehavior
 
   constructor(options: RouterOptions) {
     this.history = options.history
     // this.history.ensureLocation()
+    this.scrollBehavior = options.scrollBehavior
 
     this.matcher = new RouterMatcher(options.routes)
 
@@ -77,6 +94,7 @@ export class Router {
           ...matchedRoute,
         }
         this.updateReactiveRoute()
+        this.handleScroll(toLocation, this.pendingLocation)
       } catch (error) {
         if (NavigationGuardRedirect.is(error)) {
           // TODO: refactor the duplication of new NavigationCancelled by
@@ -112,7 +130,6 @@ export class Router {
     })
   }
 
-  // TODO: rename to resolveLocation?
   resolveLocation(
     location: MatcherLocation & Required<RouteQueryAndHash>,
     currentLocation: RouteLocationNormalized,
@@ -491,4 +508,21 @@ export class Router {
 
     return this.currentRoute
   }
+
+  private async handleScroll(
+    to: RouteLocationNormalized,
+    from: RouteLocationNormalized
+  ) {
+    if (!this.scrollBehavior) return
+    // TODO: handle other histories
+    const { state } = window.history
+    if (!state) return
+    const scroll: ScrollToPosition | void = state.scroll
+    if (!scroll) return
+
+    await this.app.$nextTick()
+    const position = await this.scrollBehavior(to, from, scroll)
+    console.log('scrolling to', position)
+    scrollToPosition(position)
+  }
 }
diff --git a/src/utils/scroll.ts b/src/utils/scroll.ts
new file mode 100644 (file)
index 0000000..8876d1e
--- /dev/null
@@ -0,0 +1,67 @@
+// import { RouteLocationNormalized } from '../types'
+
+export interface ScrollToPosition {
+  x: number
+  y: number
+}
+
+export interface ScrollToElement {
+  selector: string
+  offset?: ScrollToPosition
+}
+
+export type ScrollPosition = ScrollToPosition | ScrollToElement
+
+export function computeScrollPosition(el?: Element): ScrollToPosition {
+  return el
+    ? {
+        x: el.scrollLeft,
+        y: el.scrollTop,
+      }
+    : {
+        x: window.pageXOffset,
+        y: window.pageYOffset,
+      }
+}
+
+function getElementPosition(
+  el: Element,
+  offset: ScrollToPosition
+): ScrollToPosition {
+  const docEl = document.documentElement
+  const docRect = docEl.getBoundingClientRect()
+  const elRect = el.getBoundingClientRect()
+  return {
+    x: elRect.left - docRect.left - offset.x,
+    y: elRect.top - docRect.top - offset.y,
+  }
+}
+
+const hashStartsWithNumberRE = /^#\d/
+
+export function scrollToPosition(position: ScrollPosition): void {
+  let normalizedPosition: ScrollToPosition | null = null
+
+  if ('selector' in position) {
+    // getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
+    // but at the same time, it doesn't make much sense to select an element with an id and an extra selector
+    const el = hashStartsWithNumberRE.test(position.selector)
+      ? document.getElementById(position.selector.slice(1))
+      : document.querySelector(position.selector)
+
+    if (el) {
+      const offset: ScrollToPosition = position.offset || { x: 0, y: 0 }
+      normalizedPosition = getElementPosition(el, offset)
+    }
+    // TODO: else dev warning?
+  } else {
+    normalizedPosition = {
+      x: position.x,
+      y: position.y,
+    }
+  }
+
+  if (normalizedPosition) {
+    window.scrollTo(normalizedPosition.x, normalizedPosition.y)
+  }
+}