/* @flow */ import type VueRouter from '../index' import { stringifyQuery } from './query' const trailingSlashRE = /\/?$/ export function createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter ): Route { const stringifyQuery = router && router.options.stringifyQuery let query: any = location.query || {} try { query = clone(query) } catch (e) {} const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), matched: record ? formatMatch(record) : [] } if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery) } return Object.freeze(route) } function clone (value) { if (Array.isArray(value)) { return value.map(clone) } else if (value && typeof value === 'object') { const res = {} for (const key in value) { res[key] = clone(value[key]) } return res } else { return value } } // the starting route that represents the initial state export const START = createRoute(null, { path: '/' }) function formatMatch (record: ?RouteRecord): Array { const res = [] while (record) { res.unshift(record) record = record.parent } return res } function getFullPath ( { path, query = {}, hash = '' }, _stringifyQuery ): string { const stringify = _stringifyQuery || stringifyQuery return (path || '/') + stringify(query) + hash } export function isSameRoute (a: Route, b: ?Route, onlyPath: ?boolean): boolean { if (b === START) { return a === b } else if (!b) { return false } else if (a.path && b.path) { return a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && (onlyPath || a.hash === b.hash && isObjectEqual(a.query, b.query)) } else if (a.name && b.name) { return ( a.name === b.name && (onlyPath || ( a.hash === b.hash && isObjectEqual(a.query, b.query) && isObjectEqual(a.params, b.params)) ) ) } else { return false } } function isObjectEqual (a = {}, b = {}): boolean { // handle null value #1566 if (!a || !b) return a === b const aKeys = Object.keys(a).sort() const bKeys = Object.keys(b).sort() if (aKeys.length !== bKeys.length) { return false } return aKeys.every((key, i) => { const aVal = a[key] const bKey = bKeys[i] if (bKey !== key) return false const bVal = b[key] // query values can be null and undefined if (aVal == null || bVal == null) return aVal === bVal // check nested equality if (typeof aVal === 'object' && typeof bVal === 'object') { return isObjectEqual(aVal, bVal) } return String(aVal) === String(bVal) }) } export function isIncludedRoute (current: Route, target: Route): boolean { return ( current.path.replace(trailingSlashRE, '/').indexOf( target.path.replace(trailingSlashRE, '/') ) === 0 && (!target.hash || current.hash === target.hash) && queryIncludes(current.query, target.query) ) } function queryIncludes (current: Dictionary, target: Dictionary): boolean { for (const key in target) { if (!(key in current)) { return false } } return true } export function handleRouteEntered (route: Route) { for (let i = 0; i < route.matched.length; i++) { const record = route.matched[i] for (const name in record.instances) { const instance = record.instances[name] const cbs = record.enteredCbs[name] if (!instance || !cbs) continue delete record.enteredCbs[name] for (let i = 0; i < cbs.length; i++) { if (!instance._isBeingDestroyed) cbs[i](instance) } } } }