]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(hash): wip implementation of hash
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 8 Jul 2019 11:52:25 +0000 (13:52 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 8 Jul 2019 11:52:27 +0000 (13:52 +0200)
Still missing basic features as pausing, back, forward, go. Currently no
way to detect back/forward navigation from user (See TODO note at the
top of hash.ts file)

explorations/html5.ts
src/history/hash.ts [new file with mode: 0644]

index ece68baa0e2f4ddd917b6973c372f9c03d86f15f..0673dc1793f2c2e91dd67fe4b5f45a868d3ea2da 100644 (file)
@@ -1,11 +1,22 @@
-import { Router, HTML5History, plugin } from '../src'
+import {
+  Router,
+  // @ts-ignore
+  HTML5History,
+  // @ts-ignore
+  HashHistory,
+  // @ts-ignore
+  AbstractHistory,
+  plugin,
+  BaseHistory,
+} from '../src'
 import { RouteComponent } from '../src/types'
 import Vue from 'vue'
 
 declare global {
   interface Window {
     vm: Vue
-    h: HTML5History
+    // h: HTML5History
+    h: BaseHistory
     r: Router
   }
 }
@@ -36,9 +47,10 @@ const GuardedWithLeave: RouteComponent = {
   },
 }
 
-const html5History = new HTML5History()
+// const hist = new HTML5History()
+const hist = new HashHistory()
 const router = new Router({
-  history: html5History,
+  history: hist,
   routes: [
     { path: '/', component: Home },
     { path: '/users/:id', name: 'user', component: User },
@@ -70,7 +82,7 @@ const router = new Router({
 
 // for testing purposes
 const r = router
-const h = html5History
+const h = hist
 window.h = h
 window.r = r
 
diff --git a/src/history/hash.ts b/src/history/hash.ts
new file mode 100644 (file)
index 0000000..dae2686
--- /dev/null
@@ -0,0 +1,174 @@
+import consola from '../consola'
+import { BaseHistory, HistoryLocationNormalized, HistoryLocation } from './base'
+import { NavigationCallback, HistoryState, NavigationDirection } from './base'
+
+const cs = consola.withTag('hash')
+
+// TODO: implement the mock instead
+/* istanbul ignore next */
+// @ts-ignore otherwise fails after rollup replacement plugin
+if (process.env.NODE_ENV === 'test') cs.mockTypes(() => jest.fn())
+
+type HashChangeHandler = (this: Window, ev: HashChangeEvent) => any
+
+/**
+ * TODO: currently, we cannot prevent a hashchange, we could pass a callback to restore previous navigation on the listener. But we will face the same problems as with HTML5: go(-n) can leads to unexpected directions. We could save a copy of the history and the state, pretty much polyfilling the state stack
+ */
+
+interface PauseState {
+  currentLocation: HistoryLocationNormalized
+  // location we are going to after pausing
+  to: HistoryLocationNormalized
+}
+
+export class HashHistory extends BaseHistory {
+  // private history = window.history
+  private _hashChangeHandler: HashChangeHandler
+  private _listeners: NavigationCallback[] = []
+  private _teardowns: Array<() => void> = []
+
+  // TODO: should it be a stack? a Dict. Check if the popstate listener
+  // can trigger twice
+  private pauseState: PauseState | null = null
+
+  constructor() {
+    super()
+    // const to = this.createCurrentLocation()
+    // replace current url to ensure leading slash
+    // this.history.replaceState(buildState(null, to, null), '', to.fullPath)
+    // we cannot use window.location.hash because some browsers
+    // predecode it
+    this.location = this.utils.normalizeLocation(
+      getFullPath(window.location.href)
+    )
+    this._hashChangeHandler = this.setupHashListener()
+  }
+
+  // TODO: is this necessary
+  ensureLocation() {}
+
+  replace(location: HistoryLocation) {
+    const to = this.utils.normalizeLocation(location)
+    // this.pauseListeners(to)
+    const hashIndex = window.location.href.indexOf('#')
+    // set it before to make sure we can skip the listener with a simple check
+    this.location = to
+    window.location.replace(
+      window.location.href.slice(0, hashIndex < 0 ? 0 : hashIndex) +
+        '#' +
+        to.fullPath
+    )
+  }
+
+  push(location: HistoryLocation, data?: HistoryState) {
+    const to = this.utils.normalizeLocation(location)
+    // set it before to make sure we can skip the listener with a simple check
+    this.location = to
+    window.location.hash = '#' + to.fullPath
+  }
+
+  back(triggerListeners: boolean = true) {
+    // TODO: check if we can go back
+    // const previvousLocation = this.history.state
+    //   .back as HistoryLocationNormalized
+    if (!triggerListeners) this.pauseListeners(this.location)
+    window.history.back()
+  }
+
+  forward(triggerListeners: boolean = true) {}
+
+  listen(callback: NavigationCallback) {
+    // settup the listener and prepare teardown callbacks
+    this._listeners.push(callback)
+
+    const teardown = () => {
+      this._listeners.splice(this._listeners.indexOf(callback), 1)
+    }
+
+    this._teardowns.push(teardown)
+    return teardown
+  }
+
+  /**
+   * Remove all listeners attached to the history and cleanups the history
+   * instance
+   */
+  destroy() {
+    for (const teardown of this._teardowns) teardown()
+    this._teardowns = []
+    if (this._hashChangeHandler)
+      window.removeEventListener('hashchange', this._hashChangeHandler)
+  }
+
+  /**
+   * Setups the popstate event listener. It's important to setup only
+   * one to ensure the same parameters are passed to every listener
+   */
+  private setupHashListener() {
+    const handler: HashChangeHandler = ({ oldURL, newURL }) => {
+      // TODO: assert oldURL === this.location.fullPath
+      cs.info('hashchange fired', {
+        location: this.location.fullPath,
+        oldURL,
+        newURL,
+      })
+
+      // TODO: handle go(-2) and go(2) (skipping entries)
+
+      const from = this.location
+
+      const targetTo = getFullPath(newURL)
+
+      if (from.fullPath === targetTo) {
+        cs.info('ignored because internal navigation')
+        return
+      }
+      // we have the state from the old entry, not the current one being removed
+      // TODO: correctly parse pathname
+      // TODO: ensure newURL value is consistent
+      // handle encoding
+      const to = this.utils.normalizeLocation(targetTo)
+      this.location = to
+
+      if (
+        this.pauseState &&
+        this.pauseState.to &&
+        this.pauseState.to.fullPath === to.fullPath
+      ) {
+        cs.info('Ignored beacuse paused')
+        // reset pauseState
+        this.pauseState = null
+        return
+      }
+
+      // call all listeners
+      const navigationInfo = {
+        // TODO: should we do an unknown direction?
+        direction: NavigationDirection.forward,
+      }
+      this._listeners.forEach(listener =>
+        listener(this.location, from, navigationInfo)
+      )
+    }
+
+    // settup the listener and prepare teardown callbacks
+    window.addEventListener('hashchange', handler)
+    return handler
+  }
+
+  private pauseListeners(to: HistoryLocationNormalized) {
+    this.pauseState = {
+      currentLocation: this.location,
+      to,
+    }
+  }
+}
+
+function getFullPath(href: string): string {
+  const hashIndex = href.indexOf('#')
+  // if no hash is present, we normalize it to the version without the hash
+  const fullPath = hashIndex < 0 ? '' : href.slice(hashIndex + 1)
+
+  // ensure leading slash
+  return fullPath.indexOf('/') < 0 ? '/' + fullPath : fullPath
+}