vue-router.prod.cjs 109 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878
  1. /*!
  2. * vue-router v4.3.2
  3. * (c) 2024 Eduardo San Martin Morote
  4. * @license MIT
  5. */
  6. 'use strict';
  7. var vue = require('vue');
  8. const isBrowser = typeof document !== 'undefined';
  9. function isESModule(obj) {
  10. return obj.__esModule || obj[Symbol.toStringTag] === 'Module';
  11. }
  12. const assign = Object.assign;
  13. function applyToParams(fn, params) {
  14. const newParams = {};
  15. for (const key in params) {
  16. const value = params[key];
  17. newParams[key] = isArray(value)
  18. ? value.map(fn)
  19. : fn(value);
  20. }
  21. return newParams;
  22. }
  23. const noop = () => { };
  24. /**
  25. * Typesafe alternative to Array.isArray
  26. * https://github.com/microsoft/TypeScript/pull/48228
  27. */
  28. const isArray = Array.isArray;
  29. /**
  30. * Encoding Rules (␣ = Space)
  31. * - Path: ␣ " < > # ? { }
  32. * - Query: ␣ " < > # & =
  33. * - Hash: ␣ " < > `
  34. *
  35. * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2)
  36. * defines some extra characters to be encoded. Most browsers do not encode them
  37. * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to
  38. * also encode `!'()*`. Leaving un-encoded only ASCII alphanumeric(`a-zA-Z0-9`)
  39. * plus `-._~`. This extra safety should be applied to query by patching the
  40. * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\`
  41. * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\`
  42. * into a `/` if directly typed in. The _backtick_ (`````) should also be
  43. * encoded everywhere because some browsers like FF encode it when directly
  44. * written while others don't. Safari and IE don't encode ``"<>{}``` in hash.
  45. */
  46. // const EXTRA_RESERVED_RE = /[!'()*]/g
  47. // const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16)
  48. const HASH_RE = /#/g; // %23
  49. const AMPERSAND_RE = /&/g; // %26
  50. const SLASH_RE = /\//g; // %2F
  51. const EQUAL_RE = /=/g; // %3D
  52. const IM_RE = /\?/g; // %3F
  53. const PLUS_RE = /\+/g; // %2B
  54. /**
  55. * NOTE: It's not clear to me if we should encode the + symbol in queries, it
  56. * seems to be less flexible than not doing so and I can't find out the legacy
  57. * systems requiring this for regular requests like text/html. In the standard,
  58. * the encoding of the plus character is only mentioned for
  59. * application/x-www-form-urlencoded
  60. * (https://url.spec.whatwg.org/#urlencoded-parsing) and most browsers seems lo
  61. * leave the plus character as is in queries. To be more flexible, we allow the
  62. * plus character on the query, but it can also be manually encoded by the user.
  63. *
  64. * Resources:
  65. * - https://url.spec.whatwg.org/#urlencoded-parsing
  66. * - https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20
  67. */
  68. const ENC_BRACKET_OPEN_RE = /%5B/g; // [
  69. const ENC_BRACKET_CLOSE_RE = /%5D/g; // ]
  70. const ENC_CARET_RE = /%5E/g; // ^
  71. const ENC_BACKTICK_RE = /%60/g; // `
  72. const ENC_CURLY_OPEN_RE = /%7B/g; // {
  73. const ENC_PIPE_RE = /%7C/g; // |
  74. const ENC_CURLY_CLOSE_RE = /%7D/g; // }
  75. const ENC_SPACE_RE = /%20/g; // }
  76. /**
  77. * Encode characters that need to be encoded on the path, search and hash
  78. * sections of the URL.
  79. *
  80. * @internal
  81. * @param text - string to encode
  82. * @returns encoded string
  83. */
  84. function commonEncode(text) {
  85. return encodeURI('' + text)
  86. .replace(ENC_PIPE_RE, '|')
  87. .replace(ENC_BRACKET_OPEN_RE, '[')
  88. .replace(ENC_BRACKET_CLOSE_RE, ']');
  89. }
  90. /**
  91. * Encode characters that need to be encoded on the hash section of the URL.
  92. *
  93. * @param text - string to encode
  94. * @returns encoded string
  95. */
  96. function encodeHash(text) {
  97. return commonEncode(text)
  98. .replace(ENC_CURLY_OPEN_RE, '{')
  99. .replace(ENC_CURLY_CLOSE_RE, '}')
  100. .replace(ENC_CARET_RE, '^');
  101. }
  102. /**
  103. * Encode characters that need to be encoded query values on the query
  104. * section of the URL.
  105. *
  106. * @param text - string to encode
  107. * @returns encoded string
  108. */
  109. function encodeQueryValue(text) {
  110. return (commonEncode(text)
  111. // Encode the space as +, encode the + to differentiate it from the space
  112. .replace(PLUS_RE, '%2B')
  113. .replace(ENC_SPACE_RE, '+')
  114. .replace(HASH_RE, '%23')
  115. .replace(AMPERSAND_RE, '%26')
  116. .replace(ENC_BACKTICK_RE, '`')
  117. .replace(ENC_CURLY_OPEN_RE, '{')
  118. .replace(ENC_CURLY_CLOSE_RE, '}')
  119. .replace(ENC_CARET_RE, '^'));
  120. }
  121. /**
  122. * Like `encodeQueryValue` but also encodes the `=` character.
  123. *
  124. * @param text - string to encode
  125. */
  126. function encodeQueryKey(text) {
  127. return encodeQueryValue(text).replace(EQUAL_RE, '%3D');
  128. }
  129. /**
  130. * Encode characters that need to be encoded on the path section of the URL.
  131. *
  132. * @param text - string to encode
  133. * @returns encoded string
  134. */
  135. function encodePath(text) {
  136. return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F');
  137. }
  138. /**
  139. * Encode characters that need to be encoded on the path section of the URL as a
  140. * param. This function encodes everything {@link encodePath} does plus the
  141. * slash (`/`) character. If `text` is `null` or `undefined`, returns an empty
  142. * string instead.
  143. *
  144. * @param text - string to encode
  145. * @returns encoded string
  146. */
  147. function encodeParam(text) {
  148. return text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F');
  149. }
  150. /**
  151. * Decode text using `decodeURIComponent`. Returns the original text if it
  152. * fails.
  153. *
  154. * @param text - string to decode
  155. * @returns decoded string
  156. */
  157. function decode(text) {
  158. try {
  159. return decodeURIComponent('' + text);
  160. }
  161. catch (err) {
  162. }
  163. return '' + text;
  164. }
  165. const TRAILING_SLASH_RE = /\/$/;
  166. const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, '');
  167. /**
  168. * Transforms a URI into a normalized history location
  169. *
  170. * @param parseQuery
  171. * @param location - URI to normalize
  172. * @param currentLocation - current absolute location. Allows resolving relative
  173. * paths. Must start with `/`. Defaults to `/`
  174. * @returns a normalized history location
  175. */
  176. function parseURL(parseQuery, location, currentLocation = '/') {
  177. let path, query = {}, searchString = '', hash = '';
  178. // Could use URL and URLSearchParams but IE 11 doesn't support it
  179. // TODO: move to new URL()
  180. const hashPos = location.indexOf('#');
  181. let searchPos = location.indexOf('?');
  182. // the hash appears before the search, so it's not part of the search string
  183. if (hashPos < searchPos && hashPos >= 0) {
  184. searchPos = -1;
  185. }
  186. if (searchPos > -1) {
  187. path = location.slice(0, searchPos);
  188. searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
  189. query = parseQuery(searchString);
  190. }
  191. if (hashPos > -1) {
  192. path = path || location.slice(0, hashPos);
  193. // keep the # character
  194. hash = location.slice(hashPos, location.length);
  195. }
  196. // no search and no query
  197. path = resolveRelativePath(path != null ? path : location, currentLocation);
  198. // empty path means a relative query or hash `?foo=f`, `#thing`
  199. return {
  200. fullPath: path + (searchString && '?') + searchString + hash,
  201. path,
  202. query,
  203. hash: decode(hash),
  204. };
  205. }
  206. /**
  207. * Stringifies a URL object
  208. *
  209. * @param stringifyQuery
  210. * @param location
  211. */
  212. function stringifyURL(stringifyQuery, location) {
  213. const query = location.query ? stringifyQuery(location.query) : '';
  214. return location.path + (query && '?') + query + (location.hash || '');
  215. }
  216. /**
  217. * Strips off the base from the beginning of a location.pathname in a non-case-sensitive way.
  218. *
  219. * @param pathname - location.pathname
  220. * @param base - base to strip off
  221. */
  222. function stripBase(pathname, base) {
  223. // no base or base is not found at the beginning
  224. if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase()))
  225. return pathname;
  226. return pathname.slice(base.length) || '/';
  227. }
  228. /**
  229. * Checks if two RouteLocation are equal. This means that both locations are
  230. * pointing towards the same {@link RouteRecord} and that all `params`, `query`
  231. * parameters and `hash` are the same
  232. *
  233. * @param stringifyQuery - A function that takes a query object of type LocationQueryRaw and returns a string representation of it.
  234. * @param a - first {@link RouteLocation}
  235. * @param b - second {@link RouteLocation}
  236. */
  237. function isSameRouteLocation(stringifyQuery, a, b) {
  238. const aLastIndex = a.matched.length - 1;
  239. const bLastIndex = b.matched.length - 1;
  240. return (aLastIndex > -1 &&
  241. aLastIndex === bLastIndex &&
  242. isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
  243. isSameRouteLocationParams(a.params, b.params) &&
  244. stringifyQuery(a.query) === stringifyQuery(b.query) &&
  245. a.hash === b.hash);
  246. }
  247. /**
  248. * Check if two `RouteRecords` are equal. Takes into account aliases: they are
  249. * considered equal to the `RouteRecord` they are aliasing.
  250. *
  251. * @param a - first {@link RouteRecord}
  252. * @param b - second {@link RouteRecord}
  253. */
  254. function isSameRouteRecord(a, b) {
  255. // since the original record has an undefined value for aliasOf
  256. // but all aliases point to the original record, this will always compare
  257. // the original record
  258. return (a.aliasOf || a) === (b.aliasOf || b);
  259. }
  260. function isSameRouteLocationParams(a, b) {
  261. if (Object.keys(a).length !== Object.keys(b).length)
  262. return false;
  263. for (const key in a) {
  264. if (!isSameRouteLocationParamsValue(a[key], b[key]))
  265. return false;
  266. }
  267. return true;
  268. }
  269. function isSameRouteLocationParamsValue(a, b) {
  270. return isArray(a)
  271. ? isEquivalentArray(a, b)
  272. : isArray(b)
  273. ? isEquivalentArray(b, a)
  274. : a === b;
  275. }
  276. /**
  277. * Check if two arrays are the same or if an array with one single entry is the
  278. * same as another primitive value. Used to check query and parameters
  279. *
  280. * @param a - array of values
  281. * @param b - array of values or a single value
  282. */
  283. function isEquivalentArray(a, b) {
  284. return isArray(b)
  285. ? a.length === b.length && a.every((value, i) => value === b[i])
  286. : a.length === 1 && a[0] === b;
  287. }
  288. /**
  289. * Resolves a relative path that starts with `.`.
  290. *
  291. * @param to - path location we are resolving
  292. * @param from - currentLocation.path, should start with `/`
  293. */
  294. function resolveRelativePath(to, from) {
  295. if (to.startsWith('/'))
  296. return to;
  297. if (!to)
  298. return from;
  299. const fromSegments = from.split('/');
  300. const toSegments = to.split('/');
  301. const lastToSegment = toSegments[toSegments.length - 1];
  302. // make . and ./ the same (../ === .., ../../ === ../..)
  303. // this is the same behavior as new URL()
  304. if (lastToSegment === '..' || lastToSegment === '.') {
  305. toSegments.push('');
  306. }
  307. let position = fromSegments.length - 1;
  308. let toPosition;
  309. let segment;
  310. for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
  311. segment = toSegments[toPosition];
  312. // we stay on the same position
  313. if (segment === '.')
  314. continue;
  315. // go up in the from array
  316. if (segment === '..') {
  317. // we can't go below zero, but we still need to increment toPosition
  318. if (position > 1)
  319. position--;
  320. // continue
  321. }
  322. // we reached a non-relative path, we stop here
  323. else
  324. break;
  325. }
  326. return (fromSegments.slice(0, position).join('/') +
  327. '/' +
  328. toSegments.slice(toPosition).join('/'));
  329. }
  330. var NavigationType;
  331. (function (NavigationType) {
  332. NavigationType["pop"] = "pop";
  333. NavigationType["push"] = "push";
  334. })(NavigationType || (NavigationType = {}));
  335. var NavigationDirection;
  336. (function (NavigationDirection) {
  337. NavigationDirection["back"] = "back";
  338. NavigationDirection["forward"] = "forward";
  339. NavigationDirection["unknown"] = "";
  340. })(NavigationDirection || (NavigationDirection = {}));
  341. /**
  342. * Starting location for Histories
  343. */
  344. const START = '';
  345. // Generic utils
  346. /**
  347. * Normalizes a base by removing any trailing slash and reading the base tag if
  348. * present.
  349. *
  350. * @param base - base to normalize
  351. */
  352. function normalizeBase(base) {
  353. if (!base) {
  354. if (isBrowser) {
  355. // respect <base> tag
  356. const baseEl = document.querySelector('base');
  357. base = (baseEl && baseEl.getAttribute('href')) || '/';
  358. // strip full URL origin
  359. base = base.replace(/^\w+:\/\/[^\/]+/, '');
  360. }
  361. else {
  362. base = '/';
  363. }
  364. }
  365. // ensure leading slash when it was removed by the regex above avoid leading
  366. // slash with hash because the file could be read from the disk like file://
  367. // and the leading slash would cause problems
  368. if (base[0] !== '/' && base[0] !== '#')
  369. base = '/' + base;
  370. // remove the trailing slash so all other method can just do `base + fullPath`
  371. // to build an href
  372. return removeTrailingSlash(base);
  373. }
  374. // remove any character before the hash
  375. const BEFORE_HASH_RE = /^[^#]+#/;
  376. function createHref(base, location) {
  377. return base.replace(BEFORE_HASH_RE, '#') + location;
  378. }
  379. function getElementPosition(el, offset) {
  380. const docRect = document.documentElement.getBoundingClientRect();
  381. const elRect = el.getBoundingClientRect();
  382. return {
  383. behavior: offset.behavior,
  384. left: elRect.left - docRect.left - (offset.left || 0),
  385. top: elRect.top - docRect.top - (offset.top || 0),
  386. };
  387. }
  388. const computeScrollPosition = () => ({
  389. left: window.scrollX,
  390. top: window.scrollY,
  391. });
  392. function scrollToPosition(position) {
  393. let scrollToOptions;
  394. if ('el' in position) {
  395. const positionEl = position.el;
  396. const isIdSelector = typeof positionEl === 'string' && positionEl.startsWith('#');
  397. const el = typeof positionEl === 'string'
  398. ? isIdSelector
  399. ? document.getElementById(positionEl.slice(1))
  400. : document.querySelector(positionEl)
  401. : positionEl;
  402. if (!el) {
  403. return;
  404. }
  405. scrollToOptions = getElementPosition(el, position);
  406. }
  407. else {
  408. scrollToOptions = position;
  409. }
  410. if ('scrollBehavior' in document.documentElement.style)
  411. window.scrollTo(scrollToOptions);
  412. else {
  413. window.scrollTo(scrollToOptions.left != null ? scrollToOptions.left : window.scrollX, scrollToOptions.top != null ? scrollToOptions.top : window.scrollY);
  414. }
  415. }
  416. function getScrollKey(path, delta) {
  417. const position = history.state ? history.state.position - delta : -1;
  418. return position + path;
  419. }
  420. const scrollPositions = new Map();
  421. function saveScrollPosition(key, scrollPosition) {
  422. scrollPositions.set(key, scrollPosition);
  423. }
  424. function getSavedScrollPosition(key) {
  425. const scroll = scrollPositions.get(key);
  426. // consume it so it's not used again
  427. scrollPositions.delete(key);
  428. return scroll;
  429. }
  430. // TODO: RFC about how to save scroll position
  431. /**
  432. * ScrollBehavior instance used by the router to compute and restore the scroll
  433. * position when navigating.
  434. */
  435. // export interface ScrollHandler<ScrollPositionEntry extends HistoryStateValue, ScrollPosition extends ScrollPositionEntry> {
  436. // // returns a scroll position that can be saved in history
  437. // compute(): ScrollPositionEntry
  438. // // can take an extended ScrollPositionEntry
  439. // scroll(position: ScrollPosition): void
  440. // }
  441. // export const scrollHandler: ScrollHandler<ScrollPosition> = {
  442. // compute: computeScroll,
  443. // scroll: scrollToPosition,
  444. // }
  445. let createBaseLocation = () => location.protocol + '//' + location.host;
  446. /**
  447. * Creates a normalized history location from a window.location object
  448. * @param base - The base path
  449. * @param location - The window.location object
  450. */
  451. function createCurrentLocation(base, location) {
  452. const { pathname, search, hash } = location;
  453. // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
  454. const hashPos = base.indexOf('#');
  455. if (hashPos > -1) {
  456. let slicePos = hash.includes(base.slice(hashPos))
  457. ? base.slice(hashPos).length
  458. : 1;
  459. let pathFromHash = hash.slice(slicePos);
  460. // prepend the starting slash to hash so the url starts with /#
  461. if (pathFromHash[0] !== '/')
  462. pathFromHash = '/' + pathFromHash;
  463. return stripBase(pathFromHash, '');
  464. }
  465. const path = stripBase(pathname, base);
  466. return path + search + hash;
  467. }
  468. function useHistoryListeners(base, historyState, currentLocation, replace) {
  469. let listeners = [];
  470. let teardowns = [];
  471. // TODO: should it be a stack? a Dict. Check if the popstate listener
  472. // can trigger twice
  473. let pauseState = null;
  474. const popStateHandler = ({ state, }) => {
  475. const to = createCurrentLocation(base, location);
  476. const from = currentLocation.value;
  477. const fromState = historyState.value;
  478. let delta = 0;
  479. if (state) {
  480. currentLocation.value = to;
  481. historyState.value = state;
  482. // ignore the popstate and reset the pauseState
  483. if (pauseState && pauseState === from) {
  484. pauseState = null;
  485. return;
  486. }
  487. delta = fromState ? state.position - fromState.position : 0;
  488. }
  489. else {
  490. replace(to);
  491. }
  492. // Here we could also revert the navigation by calling history.go(-delta)
  493. // this listener will have to be adapted to not trigger again and to wait for the url
  494. // to be updated before triggering the listeners. Some kind of validation function would also
  495. // need to be passed to the listeners so the navigation can be accepted
  496. // call all listeners
  497. listeners.forEach(listener => {
  498. listener(currentLocation.value, from, {
  499. delta,
  500. type: NavigationType.pop,
  501. direction: delta
  502. ? delta > 0
  503. ? NavigationDirection.forward
  504. : NavigationDirection.back
  505. : NavigationDirection.unknown,
  506. });
  507. });
  508. };
  509. function pauseListeners() {
  510. pauseState = currentLocation.value;
  511. }
  512. function listen(callback) {
  513. // set up the listener and prepare teardown callbacks
  514. listeners.push(callback);
  515. const teardown = () => {
  516. const index = listeners.indexOf(callback);
  517. if (index > -1)
  518. listeners.splice(index, 1);
  519. };
  520. teardowns.push(teardown);
  521. return teardown;
  522. }
  523. function beforeUnloadListener() {
  524. const { history } = window;
  525. if (!history.state)
  526. return;
  527. history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), '');
  528. }
  529. function destroy() {
  530. for (const teardown of teardowns)
  531. teardown();
  532. teardowns = [];
  533. window.removeEventListener('popstate', popStateHandler);
  534. window.removeEventListener('beforeunload', beforeUnloadListener);
  535. }
  536. // set up the listeners and prepare teardown callbacks
  537. window.addEventListener('popstate', popStateHandler);
  538. // TODO: could we use 'pagehide' or 'visibilitychange' instead?
  539. // https://developer.chrome.com/blog/page-lifecycle-api/
  540. window.addEventListener('beforeunload', beforeUnloadListener, {
  541. passive: true,
  542. });
  543. return {
  544. pauseListeners,
  545. listen,
  546. destroy,
  547. };
  548. }
  549. /**
  550. * Creates a state object
  551. */
  552. function buildState(back, current, forward, replaced = false, computeScroll = false) {
  553. return {
  554. back,
  555. current,
  556. forward,
  557. replaced,
  558. position: window.history.length,
  559. scroll: computeScroll ? computeScrollPosition() : null,
  560. };
  561. }
  562. function useHistoryStateNavigation(base) {
  563. const { history, location } = window;
  564. // private variables
  565. const currentLocation = {
  566. value: createCurrentLocation(base, location),
  567. };
  568. const historyState = { value: history.state };
  569. // build current history entry as this is a fresh navigation
  570. if (!historyState.value) {
  571. changeLocation(currentLocation.value, {
  572. back: null,
  573. current: currentLocation.value,
  574. forward: null,
  575. // the length is off by one, we need to decrease it
  576. position: history.length - 1,
  577. replaced: true,
  578. // don't add a scroll as the user may have an anchor, and we want
  579. // scrollBehavior to be triggered without a saved position
  580. scroll: null,
  581. }, true);
  582. }
  583. function changeLocation(to, state, replace) {
  584. /**
  585. * if a base tag is provided, and we are on a normal domain, we have to
  586. * respect the provided `base` attribute because pushState() will use it and
  587. * potentially erase anything before the `#` like at
  588. * https://github.com/vuejs/router/issues/685 where a base of
  589. * `/folder/#` but a base of `/` would erase the `/folder/` section. If
  590. * there is no host, the `<base>` tag makes no sense and if there isn't a
  591. * base tag we can just use everything after the `#`.
  592. */
  593. const hashIndex = base.indexOf('#');
  594. const url = hashIndex > -1
  595. ? (location.host && document.querySelector('base')
  596. ? base
  597. : base.slice(hashIndex)) + to
  598. : createBaseLocation() + base + to;
  599. try {
  600. // BROWSER QUIRK
  601. // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
  602. history[replace ? 'replaceState' : 'pushState'](state, '', url);
  603. historyState.value = state;
  604. }
  605. catch (err) {
  606. {
  607. console.error(err);
  608. }
  609. // Force the navigation, this also resets the call count
  610. location[replace ? 'replace' : 'assign'](url);
  611. }
  612. }
  613. function replace(to, data) {
  614. const state = assign({}, history.state, buildState(historyState.value.back,
  615. // keep back and forward entries but override current position
  616. to, historyState.value.forward, true), data, { position: historyState.value.position });
  617. changeLocation(to, state, true);
  618. currentLocation.value = to;
  619. }
  620. function push(to, data) {
  621. // Add to current entry the information of where we are going
  622. // as well as saving the current position
  623. const currentState = assign({},
  624. // use current history state to gracefully handle a wrong call to
  625. // history.replaceState
  626. // https://github.com/vuejs/router/issues/366
  627. historyState.value, history.state, {
  628. forward: to,
  629. scroll: computeScrollPosition(),
  630. });
  631. changeLocation(currentState.current, currentState, true);
  632. const state = assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data);
  633. changeLocation(to, state, false);
  634. currentLocation.value = to;
  635. }
  636. return {
  637. location: currentLocation,
  638. state: historyState,
  639. push,
  640. replace,
  641. };
  642. }
  643. /**
  644. * Creates an HTML5 history. Most common history for single page applications.
  645. *
  646. * @param base -
  647. */
  648. function createWebHistory(base) {
  649. base = normalizeBase(base);
  650. const historyNavigation = useHistoryStateNavigation(base);
  651. const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
  652. function go(delta, triggerListeners = true) {
  653. if (!triggerListeners)
  654. historyListeners.pauseListeners();
  655. history.go(delta);
  656. }
  657. const routerHistory = assign({
  658. // it's overridden right after
  659. location: '',
  660. base,
  661. go,
  662. createHref: createHref.bind(null, base),
  663. }, historyNavigation, historyListeners);
  664. Object.defineProperty(routerHistory, 'location', {
  665. enumerable: true,
  666. get: () => historyNavigation.location.value,
  667. });
  668. Object.defineProperty(routerHistory, 'state', {
  669. enumerable: true,
  670. get: () => historyNavigation.state.value,
  671. });
  672. return routerHistory;
  673. }
  674. /**
  675. * Creates an in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere.
  676. * It's up to the user to replace that location with the starter location by either calling `router.push` or `router.replace`.
  677. *
  678. * @param base - Base applied to all urls, defaults to '/'
  679. * @returns a history object that can be passed to the router constructor
  680. */
  681. function createMemoryHistory(base = '') {
  682. let listeners = [];
  683. let queue = [START];
  684. let position = 0;
  685. base = normalizeBase(base);
  686. function setLocation(location) {
  687. position++;
  688. if (position !== queue.length) {
  689. // we are in the middle, we remove everything from here in the queue
  690. queue.splice(position);
  691. }
  692. queue.push(location);
  693. }
  694. function triggerListeners(to, from, { direction, delta }) {
  695. const info = {
  696. direction,
  697. delta,
  698. type: NavigationType.pop,
  699. };
  700. for (const callback of listeners) {
  701. callback(to, from, info);
  702. }
  703. }
  704. const routerHistory = {
  705. // rewritten by Object.defineProperty
  706. location: START,
  707. // TODO: should be kept in queue
  708. state: {},
  709. base,
  710. createHref: createHref.bind(null, base),
  711. replace(to) {
  712. // remove current entry and decrement position
  713. queue.splice(position--, 1);
  714. setLocation(to);
  715. },
  716. push(to, data) {
  717. setLocation(to);
  718. },
  719. listen(callback) {
  720. listeners.push(callback);
  721. return () => {
  722. const index = listeners.indexOf(callback);
  723. if (index > -1)
  724. listeners.splice(index, 1);
  725. };
  726. },
  727. destroy() {
  728. listeners = [];
  729. queue = [START];
  730. position = 0;
  731. },
  732. go(delta, shouldTrigger = true) {
  733. const from = this.location;
  734. const direction =
  735. // we are considering delta === 0 going forward, but in abstract mode
  736. // using 0 for the delta doesn't make sense like it does in html5 where
  737. // it reloads the page
  738. delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
  739. position = Math.max(0, Math.min(position + delta, queue.length - 1));
  740. if (shouldTrigger) {
  741. triggerListeners(this.location, from, {
  742. direction,
  743. delta,
  744. });
  745. }
  746. },
  747. };
  748. Object.defineProperty(routerHistory, 'location', {
  749. enumerable: true,
  750. get: () => queue[position],
  751. });
  752. return routerHistory;
  753. }
  754. /**
  755. * Creates a hash history. Useful for web applications with no host (e.g. `file://`) or when configuring a server to
  756. * handle any URL is not possible.
  757. *
  758. * @param base - optional base to provide. Defaults to `location.pathname + location.search` If there is a `<base>` tag
  759. * in the `head`, its value will be ignored in favor of this parameter **but note it affects all the history.pushState()
  760. * calls**, meaning that if you use a `<base>` tag, it's `href` value **has to match this parameter** (ignoring anything
  761. * after the `#`).
  762. *
  763. * @example
  764. * ```js
  765. * // at https://example.com/folder
  766. * createWebHashHistory() // gives a url of `https://example.com/folder#`
  767. * createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#`
  768. * // if the `#` is provided in the base, it won't be added by `createWebHashHistory`
  769. * createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/`
  770. * // you should avoid doing this because it changes the original url and breaks copying urls
  771. * createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#`
  772. *
  773. * // at file:///usr/etc/folder/index.html
  774. * // for locations with no `host`, the base is ignored
  775. * createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#`
  776. * ```
  777. */
  778. function createWebHashHistory(base) {
  779. // Make sure this implementation is fine in terms of encoding, specially for IE11
  780. // for `file://`, directly use the pathname and ignore the base
  781. // location.pathname contains an initial `/` even at the root: `https://example.com`
  782. base = location.host ? base || location.pathname + location.search : '';
  783. // allow the user to provide a `#` in the middle: `/base/#/app`
  784. if (!base.includes('#'))
  785. base += '#';
  786. return createWebHistory(base);
  787. }
  788. function isRouteLocation(route) {
  789. return typeof route === 'string' || (route && typeof route === 'object');
  790. }
  791. function isRouteName(name) {
  792. return typeof name === 'string' || typeof name === 'symbol';
  793. }
  794. /**
  795. * Initial route location where the router is. Can be used in navigation guards
  796. * to differentiate the initial navigation.
  797. *
  798. * @example
  799. * ```js
  800. * import { START_LOCATION } from 'vue-router'
  801. *
  802. * router.beforeEach((to, from) => {
  803. * if (from === START_LOCATION) {
  804. * // initial navigation
  805. * }
  806. * })
  807. * ```
  808. */
  809. const START_LOCATION_NORMALIZED = {
  810. path: '/',
  811. name: undefined,
  812. params: {},
  813. query: {},
  814. hash: '',
  815. fullPath: '/',
  816. matched: [],
  817. meta: {},
  818. redirectedFrom: undefined,
  819. };
  820. const NavigationFailureSymbol = Symbol('');
  821. /**
  822. * Enumeration with all possible types for navigation failures. Can be passed to
  823. * {@link isNavigationFailure} to check for specific failures.
  824. */
  825. exports.NavigationFailureType = void 0;
  826. (function (NavigationFailureType) {
  827. /**
  828. * An aborted navigation is a navigation that failed because a navigation
  829. * guard returned `false` or called `next(false)`
  830. */
  831. NavigationFailureType[NavigationFailureType["aborted"] = 4] = "aborted";
  832. /**
  833. * A cancelled navigation is a navigation that failed because a more recent
  834. * navigation finished started (not necessarily finished).
  835. */
  836. NavigationFailureType[NavigationFailureType["cancelled"] = 8] = "cancelled";
  837. /**
  838. * A duplicated navigation is a navigation that failed because it was
  839. * initiated while already being at the exact same location.
  840. */
  841. NavigationFailureType[NavigationFailureType["duplicated"] = 16] = "duplicated";
  842. })(exports.NavigationFailureType || (exports.NavigationFailureType = {}));
  843. // DEV only debug messages
  844. const ErrorTypeMessages = {
  845. [1 /* ErrorTypes.MATCHER_NOT_FOUND */]({ location, currentLocation }) {
  846. return `No match for\n ${JSON.stringify(location)}${currentLocation
  847. ? '\nwhile being at\n' + JSON.stringify(currentLocation)
  848. : ''}`;
  849. },
  850. [2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */]({ from, to, }) {
  851. return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`;
  852. },
  853. [4 /* ErrorTypes.NAVIGATION_ABORTED */]({ from, to }) {
  854. return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`;
  855. },
  856. [8 /* ErrorTypes.NAVIGATION_CANCELLED */]({ from, to }) {
  857. return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`;
  858. },
  859. [16 /* ErrorTypes.NAVIGATION_DUPLICATED */]({ from, to }) {
  860. return `Avoided redundant navigation to current location: "${from.fullPath}".`;
  861. },
  862. };
  863. /**
  864. * Creates a typed NavigationFailure object.
  865. * @internal
  866. * @param type - NavigationFailureType
  867. * @param params - { from, to }
  868. */
  869. function createRouterError(type, params) {
  870. // keep full error messages in cjs versions
  871. {
  872. return assign(new Error(ErrorTypeMessages[type](params)), {
  873. type,
  874. [NavigationFailureSymbol]: true,
  875. }, params);
  876. }
  877. }
  878. function isNavigationFailure(error, type) {
  879. return (error instanceof Error &&
  880. NavigationFailureSymbol in error &&
  881. (type == null || !!(error.type & type)));
  882. }
  883. const propertiesToLog = ['params', 'query', 'hash'];
  884. function stringifyRoute(to) {
  885. if (typeof to === 'string')
  886. return to;
  887. if (to.path != null)
  888. return to.path;
  889. const location = {};
  890. for (const key of propertiesToLog) {
  891. if (key in to)
  892. location[key] = to[key];
  893. }
  894. return JSON.stringify(location, null, 2);
  895. }
  896. // default pattern for a param: non-greedy everything but /
  897. const BASE_PARAM_PATTERN = '[^/]+?';
  898. const BASE_PATH_PARSER_OPTIONS = {
  899. sensitive: false,
  900. strict: false,
  901. start: true,
  902. end: true,
  903. };
  904. // Special Regex characters that must be escaped in static tokens
  905. const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
  906. /**
  907. * Creates a path parser from an array of Segments (a segment is an array of Tokens)
  908. *
  909. * @param segments - array of segments returned by tokenizePath
  910. * @param extraOptions - optional options for the regexp
  911. * @returns a PathParser
  912. */
  913. function tokensToParser(segments, extraOptions) {
  914. const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
  915. // the amount of scores is the same as the length of segments except for the root segment "/"
  916. const score = [];
  917. // the regexp as a string
  918. let pattern = options.start ? '^' : '';
  919. // extracted keys
  920. const keys = [];
  921. for (const segment of segments) {
  922. // the root segment needs special treatment
  923. const segmentScores = segment.length ? [] : [90 /* PathScore.Root */];
  924. // allow trailing slash
  925. if (options.strict && !segment.length)
  926. pattern += '/';
  927. for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
  928. const token = segment[tokenIndex];
  929. // resets the score if we are inside a sub-segment /:a-other-:b
  930. let subSegmentScore = 40 /* PathScore.Segment */ +
  931. (options.sensitive ? 0.25 /* PathScore.BonusCaseSensitive */ : 0);
  932. if (token.type === 0 /* TokenType.Static */) {
  933. // prepend the slash if we are starting a new segment
  934. if (!tokenIndex)
  935. pattern += '/';
  936. pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
  937. subSegmentScore += 40 /* PathScore.Static */;
  938. }
  939. else if (token.type === 1 /* TokenType.Param */) {
  940. const { value, repeatable, optional, regexp } = token;
  941. keys.push({
  942. name: value,
  943. repeatable,
  944. optional,
  945. });
  946. const re = regexp ? regexp : BASE_PARAM_PATTERN;
  947. // the user provided a custom regexp /:id(\\d+)
  948. if (re !== BASE_PARAM_PATTERN) {
  949. subSegmentScore += 10 /* PathScore.BonusCustomRegExp */;
  950. // make sure the regexp is valid before using it
  951. try {
  952. new RegExp(`(${re})`);
  953. }
  954. catch (err) {
  955. throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
  956. err.message);
  957. }
  958. }
  959. // when we repeat we must take care of the repeating leading slash
  960. let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
  961. // prepend the slash if we are starting a new segment
  962. if (!tokenIndex)
  963. subPattern =
  964. // avoid an optional / if there are more segments e.g. /:p?-static
  965. // or /:p?-:p2
  966. optional && segment.length < 2
  967. ? `(?:/${subPattern})`
  968. : '/' + subPattern;
  969. if (optional)
  970. subPattern += '?';
  971. pattern += subPattern;
  972. subSegmentScore += 20 /* PathScore.Dynamic */;
  973. if (optional)
  974. subSegmentScore += -8 /* PathScore.BonusOptional */;
  975. if (repeatable)
  976. subSegmentScore += -20 /* PathScore.BonusRepeatable */;
  977. if (re === '.*')
  978. subSegmentScore += -50 /* PathScore.BonusWildcard */;
  979. }
  980. segmentScores.push(subSegmentScore);
  981. }
  982. // an empty array like /home/ -> [[{home}], []]
  983. // if (!segment.length) pattern += '/'
  984. score.push(segmentScores);
  985. }
  986. // only apply the strict bonus to the last score
  987. if (options.strict && options.end) {
  988. const i = score.length - 1;
  989. score[i][score[i].length - 1] += 0.7000000000000001 /* PathScore.BonusStrict */;
  990. }
  991. // TODO: dev only warn double trailing slash
  992. if (!options.strict)
  993. pattern += '/?';
  994. if (options.end)
  995. pattern += '$';
  996. // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
  997. else if (options.strict)
  998. pattern += '(?:/|$)';
  999. const re = new RegExp(pattern, options.sensitive ? '' : 'i');
  1000. function parse(path) {
  1001. const match = path.match(re);
  1002. const params = {};
  1003. if (!match)
  1004. return null;
  1005. for (let i = 1; i < match.length; i++) {
  1006. const value = match[i] || '';
  1007. const key = keys[i - 1];
  1008. params[key.name] = value && key.repeatable ? value.split('/') : value;
  1009. }
  1010. return params;
  1011. }
  1012. function stringify(params) {
  1013. let path = '';
  1014. // for optional parameters to allow to be empty
  1015. let avoidDuplicatedSlash = false;
  1016. for (const segment of segments) {
  1017. if (!avoidDuplicatedSlash || !path.endsWith('/'))
  1018. path += '/';
  1019. avoidDuplicatedSlash = false;
  1020. for (const token of segment) {
  1021. if (token.type === 0 /* TokenType.Static */) {
  1022. path += token.value;
  1023. }
  1024. else if (token.type === 1 /* TokenType.Param */) {
  1025. const { value, repeatable, optional } = token;
  1026. const param = value in params ? params[value] : '';
  1027. if (isArray(param) && !repeatable) {
  1028. throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
  1029. }
  1030. const text = isArray(param)
  1031. ? param.join('/')
  1032. : param;
  1033. if (!text) {
  1034. if (optional) {
  1035. // if we have more than one optional param like /:a?-static we don't need to care about the optional param
  1036. if (segment.length < 2) {
  1037. // remove the last slash as we could be at the end
  1038. if (path.endsWith('/'))
  1039. path = path.slice(0, -1);
  1040. // do not append a slash on the next iteration
  1041. else
  1042. avoidDuplicatedSlash = true;
  1043. }
  1044. }
  1045. else
  1046. throw new Error(`Missing required param "${value}"`);
  1047. }
  1048. path += text;
  1049. }
  1050. }
  1051. }
  1052. // avoid empty path when we have multiple optional params
  1053. return path || '/';
  1054. }
  1055. return {
  1056. re,
  1057. score,
  1058. keys,
  1059. parse,
  1060. stringify,
  1061. };
  1062. }
  1063. /**
  1064. * Compares an array of numbers as used in PathParser.score and returns a
  1065. * number. This function can be used to `sort` an array
  1066. *
  1067. * @param a - first array of numbers
  1068. * @param b - second array of numbers
  1069. * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
  1070. * should be sorted first
  1071. */
  1072. function compareScoreArray(a, b) {
  1073. let i = 0;
  1074. while (i < a.length && i < b.length) {
  1075. const diff = b[i] - a[i];
  1076. // only keep going if diff === 0
  1077. if (diff)
  1078. return diff;
  1079. i++;
  1080. }
  1081. // if the last subsegment was Static, the shorter segments should be sorted first
  1082. // otherwise sort the longest segment first
  1083. if (a.length < b.length) {
  1084. return a.length === 1 && a[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
  1085. ? -1
  1086. : 1;
  1087. }
  1088. else if (a.length > b.length) {
  1089. return b.length === 1 && b[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
  1090. ? 1
  1091. : -1;
  1092. }
  1093. return 0;
  1094. }
  1095. /**
  1096. * Compare function that can be used with `sort` to sort an array of PathParser
  1097. *
  1098. * @param a - first PathParser
  1099. * @param b - second PathParser
  1100. * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
  1101. */
  1102. function comparePathParserScore(a, b) {
  1103. let i = 0;
  1104. const aScore = a.score;
  1105. const bScore = b.score;
  1106. while (i < aScore.length && i < bScore.length) {
  1107. const comp = compareScoreArray(aScore[i], bScore[i]);
  1108. // do not return if both are equal
  1109. if (comp)
  1110. return comp;
  1111. i++;
  1112. }
  1113. if (Math.abs(bScore.length - aScore.length) === 1) {
  1114. if (isLastScoreNegative(aScore))
  1115. return 1;
  1116. if (isLastScoreNegative(bScore))
  1117. return -1;
  1118. }
  1119. // if a and b share the same score entries but b has more, sort b first
  1120. return bScore.length - aScore.length;
  1121. // this is the ternary version
  1122. // return aScore.length < bScore.length
  1123. // ? 1
  1124. // : aScore.length > bScore.length
  1125. // ? -1
  1126. // : 0
  1127. }
  1128. /**
  1129. * This allows detecting splats at the end of a path: /home/:id(.*)*
  1130. *
  1131. * @param score - score to check
  1132. * @returns true if the last entry is negative
  1133. */
  1134. function isLastScoreNegative(score) {
  1135. const last = score[score.length - 1];
  1136. return score.length > 0 && last[last.length - 1] < 0;
  1137. }
  1138. const ROOT_TOKEN = {
  1139. type: 0 /* TokenType.Static */,
  1140. value: '',
  1141. };
  1142. const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
  1143. // After some profiling, the cache seems to be unnecessary because tokenizePath
  1144. // (the slowest part of adding a route) is very fast
  1145. // const tokenCache = new Map<string, Token[][]>()
  1146. function tokenizePath(path) {
  1147. if (!path)
  1148. return [[]];
  1149. if (path === '/')
  1150. return [[ROOT_TOKEN]];
  1151. if (!path.startsWith('/')) {
  1152. throw new Error(`Invalid path "${path}"`);
  1153. }
  1154. // if (tokenCache.has(path)) return tokenCache.get(path)!
  1155. function crash(message) {
  1156. throw new Error(`ERR (${state})/"${buffer}": ${message}`);
  1157. }
  1158. let state = 0 /* TokenizerState.Static */;
  1159. let previousState = state;
  1160. const tokens = [];
  1161. // the segment will always be valid because we get into the initial state
  1162. // with the leading /
  1163. let segment;
  1164. function finalizeSegment() {
  1165. if (segment)
  1166. tokens.push(segment);
  1167. segment = [];
  1168. }
  1169. // index on the path
  1170. let i = 0;
  1171. // char at index
  1172. let char;
  1173. // buffer of the value read
  1174. let buffer = '';
  1175. // custom regexp for a param
  1176. let customRe = '';
  1177. function consumeBuffer() {
  1178. if (!buffer)
  1179. return;
  1180. if (state === 0 /* TokenizerState.Static */) {
  1181. segment.push({
  1182. type: 0 /* TokenType.Static */,
  1183. value: buffer,
  1184. });
  1185. }
  1186. else if (state === 1 /* TokenizerState.Param */ ||
  1187. state === 2 /* TokenizerState.ParamRegExp */ ||
  1188. state === 3 /* TokenizerState.ParamRegExpEnd */) {
  1189. if (segment.length > 1 && (char === '*' || char === '+'))
  1190. crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`);
  1191. segment.push({
  1192. type: 1 /* TokenType.Param */,
  1193. value: buffer,
  1194. regexp: customRe,
  1195. repeatable: char === '*' || char === '+',
  1196. optional: char === '*' || char === '?',
  1197. });
  1198. }
  1199. else {
  1200. crash('Invalid state to consume buffer');
  1201. }
  1202. buffer = '';
  1203. }
  1204. function addCharToBuffer() {
  1205. buffer += char;
  1206. }
  1207. while (i < path.length) {
  1208. char = path[i++];
  1209. if (char === '\\' && state !== 2 /* TokenizerState.ParamRegExp */) {
  1210. previousState = state;
  1211. state = 4 /* TokenizerState.EscapeNext */;
  1212. continue;
  1213. }
  1214. switch (state) {
  1215. case 0 /* TokenizerState.Static */:
  1216. if (char === '/') {
  1217. if (buffer) {
  1218. consumeBuffer();
  1219. }
  1220. finalizeSegment();
  1221. }
  1222. else if (char === ':') {
  1223. consumeBuffer();
  1224. state = 1 /* TokenizerState.Param */;
  1225. }
  1226. else {
  1227. addCharToBuffer();
  1228. }
  1229. break;
  1230. case 4 /* TokenizerState.EscapeNext */:
  1231. addCharToBuffer();
  1232. state = previousState;
  1233. break;
  1234. case 1 /* TokenizerState.Param */:
  1235. if (char === '(') {
  1236. state = 2 /* TokenizerState.ParamRegExp */;
  1237. }
  1238. else if (VALID_PARAM_RE.test(char)) {
  1239. addCharToBuffer();
  1240. }
  1241. else {
  1242. consumeBuffer();
  1243. state = 0 /* TokenizerState.Static */;
  1244. // go back one character if we were not modifying
  1245. if (char !== '*' && char !== '?' && char !== '+')
  1246. i--;
  1247. }
  1248. break;
  1249. case 2 /* TokenizerState.ParamRegExp */:
  1250. // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
  1251. // it already works by escaping the closing )
  1252. // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
  1253. // is this really something people need since you can also write
  1254. // /prefix_:p()_suffix
  1255. if (char === ')') {
  1256. // handle the escaped )
  1257. if (customRe[customRe.length - 1] == '\\')
  1258. customRe = customRe.slice(0, -1) + char;
  1259. else
  1260. state = 3 /* TokenizerState.ParamRegExpEnd */;
  1261. }
  1262. else {
  1263. customRe += char;
  1264. }
  1265. break;
  1266. case 3 /* TokenizerState.ParamRegExpEnd */:
  1267. // same as finalizing a param
  1268. consumeBuffer();
  1269. state = 0 /* TokenizerState.Static */;
  1270. // go back one character if we were not modifying
  1271. if (char !== '*' && char !== '?' && char !== '+')
  1272. i--;
  1273. customRe = '';
  1274. break;
  1275. default:
  1276. crash('Unknown state');
  1277. break;
  1278. }
  1279. }
  1280. if (state === 2 /* TokenizerState.ParamRegExp */)
  1281. crash(`Unfinished custom RegExp for param "${buffer}"`);
  1282. consumeBuffer();
  1283. finalizeSegment();
  1284. // tokenCache.set(path, tokens)
  1285. return tokens;
  1286. }
  1287. function createRouteRecordMatcher(record, parent, options) {
  1288. const parser = tokensToParser(tokenizePath(record.path), options);
  1289. const matcher = assign(parser, {
  1290. record,
  1291. parent,
  1292. // these needs to be populated by the parent
  1293. children: [],
  1294. alias: [],
  1295. });
  1296. if (parent) {
  1297. // both are aliases or both are not aliases
  1298. // we don't want to mix them because the order is used when
  1299. // passing originalRecord in Matcher.addRoute
  1300. if (!matcher.record.aliasOf === !parent.record.aliasOf)
  1301. parent.children.push(matcher);
  1302. }
  1303. return matcher;
  1304. }
  1305. /**
  1306. * Creates a Router Matcher.
  1307. *
  1308. * @internal
  1309. * @param routes - array of initial routes
  1310. * @param globalOptions - global route options
  1311. */
  1312. function createRouterMatcher(routes, globalOptions) {
  1313. // normalized ordered array of matchers
  1314. const matchers = [];
  1315. const matcherMap = new Map();
  1316. globalOptions = mergeOptions({ strict: false, end: true, sensitive: false }, globalOptions);
  1317. function getRecordMatcher(name) {
  1318. return matcherMap.get(name);
  1319. }
  1320. function addRoute(record, parent, originalRecord) {
  1321. // used later on to remove by name
  1322. const isRootAdd = !originalRecord;
  1323. const mainNormalizedRecord = normalizeRouteRecord(record);
  1324. // we might be the child of an alias
  1325. mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record;
  1326. const options = mergeOptions(globalOptions, record);
  1327. // generate an array of records to correctly handle aliases
  1328. const normalizedRecords = [
  1329. mainNormalizedRecord,
  1330. ];
  1331. if ('alias' in record) {
  1332. const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
  1333. for (const alias of aliases) {
  1334. normalizedRecords.push(assign({}, mainNormalizedRecord, {
  1335. // this allows us to hold a copy of the `components` option
  1336. // so that async components cache is hold on the original record
  1337. components: originalRecord
  1338. ? originalRecord.record.components
  1339. : mainNormalizedRecord.components,
  1340. path: alias,
  1341. // we might be the child of an alias
  1342. aliasOf: originalRecord
  1343. ? originalRecord.record
  1344. : mainNormalizedRecord,
  1345. // the aliases are always of the same kind as the original since they
  1346. // are defined on the same record
  1347. }));
  1348. }
  1349. }
  1350. let matcher;
  1351. let originalMatcher;
  1352. for (const normalizedRecord of normalizedRecords) {
  1353. const { path } = normalizedRecord;
  1354. // Build up the path for nested routes if the child isn't an absolute
  1355. // route. Only add the / delimiter if the child path isn't empty and if the
  1356. // parent path doesn't have a trailing slash
  1357. if (parent && path[0] !== '/') {
  1358. const parentPath = parent.record.path;
  1359. const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
  1360. normalizedRecord.path =
  1361. parent.record.path + (path && connectingSlash + path);
  1362. }
  1363. // create the object beforehand, so it can be passed to children
  1364. matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
  1365. // if we are an alias we must tell the original record that we exist,
  1366. // so we can be removed
  1367. if (originalRecord) {
  1368. originalRecord.alias.push(matcher);
  1369. }
  1370. else {
  1371. // otherwise, the first record is the original and others are aliases
  1372. originalMatcher = originalMatcher || matcher;
  1373. if (originalMatcher !== matcher)
  1374. originalMatcher.alias.push(matcher);
  1375. // remove the route if named and only for the top record (avoid in nested calls)
  1376. // this works because the original record is the first one
  1377. if (isRootAdd && record.name && !isAliasRecord(matcher))
  1378. removeRoute(record.name);
  1379. }
  1380. if (mainNormalizedRecord.children) {
  1381. const children = mainNormalizedRecord.children;
  1382. for (let i = 0; i < children.length; i++) {
  1383. addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
  1384. }
  1385. }
  1386. // if there was no original record, then the first one was not an alias and all
  1387. // other aliases (if any) need to reference this record when adding children
  1388. originalRecord = originalRecord || matcher;
  1389. // TODO: add normalized records for more flexibility
  1390. // if (parent && isAliasRecord(originalRecord)) {
  1391. // parent.children.push(originalRecord)
  1392. // }
  1393. // Avoid adding a record that doesn't display anything. This allows passing through records without a component to
  1394. // not be reached and pass through the catch all route
  1395. if ((matcher.record.components &&
  1396. Object.keys(matcher.record.components).length) ||
  1397. matcher.record.name ||
  1398. matcher.record.redirect) {
  1399. insertMatcher(matcher);
  1400. }
  1401. }
  1402. return originalMatcher
  1403. ? () => {
  1404. // since other matchers are aliases, they should be removed by the original matcher
  1405. removeRoute(originalMatcher);
  1406. }
  1407. : noop;
  1408. }
  1409. function removeRoute(matcherRef) {
  1410. if (isRouteName(matcherRef)) {
  1411. const matcher = matcherMap.get(matcherRef);
  1412. if (matcher) {
  1413. matcherMap.delete(matcherRef);
  1414. matchers.splice(matchers.indexOf(matcher), 1);
  1415. matcher.children.forEach(removeRoute);
  1416. matcher.alias.forEach(removeRoute);
  1417. }
  1418. }
  1419. else {
  1420. const index = matchers.indexOf(matcherRef);
  1421. if (index > -1) {
  1422. matchers.splice(index, 1);
  1423. if (matcherRef.record.name)
  1424. matcherMap.delete(matcherRef.record.name);
  1425. matcherRef.children.forEach(removeRoute);
  1426. matcherRef.alias.forEach(removeRoute);
  1427. }
  1428. }
  1429. }
  1430. function getRoutes() {
  1431. return matchers;
  1432. }
  1433. function insertMatcher(matcher) {
  1434. let i = 0;
  1435. while (i < matchers.length &&
  1436. comparePathParserScore(matcher, matchers[i]) >= 0 &&
  1437. // Adding children with empty path should still appear before the parent
  1438. // https://github.com/vuejs/router/issues/1124
  1439. (matcher.record.path !== matchers[i].record.path ||
  1440. !isRecordChildOf(matcher, matchers[i])))
  1441. i++;
  1442. matchers.splice(i, 0, matcher);
  1443. // only add the original record to the name map
  1444. if (matcher.record.name && !isAliasRecord(matcher))
  1445. matcherMap.set(matcher.record.name, matcher);
  1446. }
  1447. function resolve(location, currentLocation) {
  1448. let matcher;
  1449. let params = {};
  1450. let path;
  1451. let name;
  1452. if ('name' in location && location.name) {
  1453. matcher = matcherMap.get(location.name);
  1454. if (!matcher)
  1455. throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, {
  1456. location,
  1457. });
  1458. name = matcher.record.name;
  1459. params = assign(
  1460. // paramsFromLocation is a new object
  1461. paramsFromLocation(currentLocation.params,
  1462. // only keep params that exist in the resolved location
  1463. // only keep optional params coming from a parent record
  1464. matcher.keys
  1465. .filter(k => !k.optional)
  1466. .concat(matcher.parent ? matcher.parent.keys.filter(k => k.optional) : [])
  1467. .map(k => k.name)),
  1468. // discard any existing params in the current location that do not exist here
  1469. // #1497 this ensures better active/exact matching
  1470. location.params &&
  1471. paramsFromLocation(location.params, matcher.keys.map(k => k.name)));
  1472. // throws if cannot be stringified
  1473. path = matcher.stringify(params);
  1474. }
  1475. else if (location.path != null) {
  1476. // no need to resolve the path with the matcher as it was provided
  1477. // this also allows the user to control the encoding
  1478. path = location.path;
  1479. matcher = matchers.find(m => m.re.test(path));
  1480. // matcher should have a value after the loop
  1481. if (matcher) {
  1482. // we know the matcher works because we tested the regexp
  1483. params = matcher.parse(path);
  1484. name = matcher.record.name;
  1485. }
  1486. // location is a relative path
  1487. }
  1488. else {
  1489. // match by name or path of current route
  1490. matcher = currentLocation.name
  1491. ? matcherMap.get(currentLocation.name)
  1492. : matchers.find(m => m.re.test(currentLocation.path));
  1493. if (!matcher)
  1494. throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, {
  1495. location,
  1496. currentLocation,
  1497. });
  1498. name = matcher.record.name;
  1499. // since we are navigating to the same location, we don't need to pick the
  1500. // params like when `name` is provided
  1501. params = assign({}, currentLocation.params, location.params);
  1502. path = matcher.stringify(params);
  1503. }
  1504. const matched = [];
  1505. let parentMatcher = matcher;
  1506. while (parentMatcher) {
  1507. // reversed order so parents are at the beginning
  1508. matched.unshift(parentMatcher.record);
  1509. parentMatcher = parentMatcher.parent;
  1510. }
  1511. return {
  1512. name,
  1513. path,
  1514. params,
  1515. matched,
  1516. meta: mergeMetaFields(matched),
  1517. };
  1518. }
  1519. // add initial routes
  1520. routes.forEach(route => addRoute(route));
  1521. return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
  1522. }
  1523. function paramsFromLocation(params, keys) {
  1524. const newParams = {};
  1525. for (const key of keys) {
  1526. if (key in params)
  1527. newParams[key] = params[key];
  1528. }
  1529. return newParams;
  1530. }
  1531. /**
  1532. * Normalizes a RouteRecordRaw. Creates a copy
  1533. *
  1534. * @param record
  1535. * @returns the normalized version
  1536. */
  1537. function normalizeRouteRecord(record) {
  1538. return {
  1539. path: record.path,
  1540. redirect: record.redirect,
  1541. name: record.name,
  1542. meta: record.meta || {},
  1543. aliasOf: undefined,
  1544. beforeEnter: record.beforeEnter,
  1545. props: normalizeRecordProps(record),
  1546. children: record.children || [],
  1547. instances: {},
  1548. leaveGuards: new Set(),
  1549. updateGuards: new Set(),
  1550. enterCallbacks: {},
  1551. components: 'components' in record
  1552. ? record.components || null
  1553. : record.component && { default: record.component },
  1554. };
  1555. }
  1556. /**
  1557. * Normalize the optional `props` in a record to always be an object similar to
  1558. * components. Also accept a boolean for components.
  1559. * @param record
  1560. */
  1561. function normalizeRecordProps(record) {
  1562. const propsObject = {};
  1563. // props does not exist on redirect records, but we can set false directly
  1564. const props = record.props || false;
  1565. if ('component' in record) {
  1566. propsObject.default = props;
  1567. }
  1568. else {
  1569. // NOTE: we could also allow a function to be applied to every component.
  1570. // Would need user feedback for use cases
  1571. for (const name in record.components)
  1572. propsObject[name] = typeof props === 'object' ? props[name] : props;
  1573. }
  1574. return propsObject;
  1575. }
  1576. /**
  1577. * Checks if a record or any of its parent is an alias
  1578. * @param record
  1579. */
  1580. function isAliasRecord(record) {
  1581. while (record) {
  1582. if (record.record.aliasOf)
  1583. return true;
  1584. record = record.parent;
  1585. }
  1586. return false;
  1587. }
  1588. /**
  1589. * Merge meta fields of an array of records
  1590. *
  1591. * @param matched - array of matched records
  1592. */
  1593. function mergeMetaFields(matched) {
  1594. return matched.reduce((meta, record) => assign(meta, record.meta), {});
  1595. }
  1596. function mergeOptions(defaults, partialOptions) {
  1597. const options = {};
  1598. for (const key in defaults) {
  1599. options[key] = key in partialOptions ? partialOptions[key] : defaults[key];
  1600. }
  1601. return options;
  1602. }
  1603. function isRecordChildOf(record, parent) {
  1604. return parent.children.some(child => child === record || isRecordChildOf(record, child));
  1605. }
  1606. /**
  1607. * Transforms a queryString into a {@link LocationQuery} object. Accept both, a
  1608. * version with the leading `?` and without Should work as URLSearchParams
  1609. * @internal
  1610. *
  1611. * @param search - search string to parse
  1612. * @returns a query object
  1613. */
  1614. function parseQuery(search) {
  1615. const query = {};
  1616. // avoid creating an object with an empty key and empty value
  1617. // because of split('&')
  1618. if (search === '' || search === '?')
  1619. return query;
  1620. const hasLeadingIM = search[0] === '?';
  1621. const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&');
  1622. for (let i = 0; i < searchParams.length; ++i) {
  1623. // pre decode the + into space
  1624. const searchParam = searchParams[i].replace(PLUS_RE, ' ');
  1625. // allow the = character
  1626. const eqPos = searchParam.indexOf('=');
  1627. const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos));
  1628. const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1));
  1629. if (key in query) {
  1630. // an extra variable for ts types
  1631. let currentValue = query[key];
  1632. if (!isArray(currentValue)) {
  1633. currentValue = query[key] = [currentValue];
  1634. }
  1635. currentValue.push(value);
  1636. }
  1637. else {
  1638. query[key] = value;
  1639. }
  1640. }
  1641. return query;
  1642. }
  1643. /**
  1644. * Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it
  1645. * doesn't prepend a `?`
  1646. *
  1647. * @internal
  1648. *
  1649. * @param query - query object to stringify
  1650. * @returns string version of the query without the leading `?`
  1651. */
  1652. function stringifyQuery(query) {
  1653. let search = '';
  1654. for (let key in query) {
  1655. const value = query[key];
  1656. key = encodeQueryKey(key);
  1657. if (value == null) {
  1658. // only null adds the value
  1659. if (value !== undefined) {
  1660. search += (search.length ? '&' : '') + key;
  1661. }
  1662. continue;
  1663. }
  1664. // keep null values
  1665. const values = isArray(value)
  1666. ? value.map(v => v && encodeQueryValue(v))
  1667. : [value && encodeQueryValue(value)];
  1668. values.forEach(value => {
  1669. // skip undefined values in arrays as if they were not present
  1670. // smaller code than using filter
  1671. if (value !== undefined) {
  1672. // only append & with non-empty search
  1673. search += (search.length ? '&' : '') + key;
  1674. if (value != null)
  1675. search += '=' + value;
  1676. }
  1677. });
  1678. }
  1679. return search;
  1680. }
  1681. /**
  1682. * Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting
  1683. * numbers into strings, removing keys with an undefined value and replacing
  1684. * undefined with null in arrays
  1685. *
  1686. * @param query - query object to normalize
  1687. * @returns a normalized query object
  1688. */
  1689. function normalizeQuery(query) {
  1690. const normalizedQuery = {};
  1691. for (const key in query) {
  1692. const value = query[key];
  1693. if (value !== undefined) {
  1694. normalizedQuery[key] = isArray(value)
  1695. ? value.map(v => (v == null ? null : '' + v))
  1696. : value == null
  1697. ? value
  1698. : '' + value;
  1699. }
  1700. }
  1701. return normalizedQuery;
  1702. }
  1703. /**
  1704. * RouteRecord being rendered by the closest ancestor Router View. Used for
  1705. * `onBeforeRouteUpdate` and `onBeforeRouteLeave`. rvlm stands for Router View
  1706. * Location Matched
  1707. *
  1708. * @internal
  1709. */
  1710. const matchedRouteKey = Symbol('');
  1711. /**
  1712. * Allows overriding the router view depth to control which component in
  1713. * `matched` is rendered. rvd stands for Router View Depth
  1714. *
  1715. * @internal
  1716. */
  1717. const viewDepthKey = Symbol('');
  1718. /**
  1719. * Allows overriding the router instance returned by `useRouter` in tests. r
  1720. * stands for router
  1721. *
  1722. * @internal
  1723. */
  1724. const routerKey = Symbol('');
  1725. /**
  1726. * Allows overriding the current route returned by `useRoute` in tests. rl
  1727. * stands for route location
  1728. *
  1729. * @internal
  1730. */
  1731. const routeLocationKey = Symbol('');
  1732. /**
  1733. * Allows overriding the current route used by router-view. Internally this is
  1734. * used when the `route` prop is passed.
  1735. *
  1736. * @internal
  1737. */
  1738. const routerViewLocationKey = Symbol('');
  1739. /**
  1740. * Create a list of callbacks that can be reset. Used to create before and after navigation guards list
  1741. */
  1742. function useCallbacks() {
  1743. let handlers = [];
  1744. function add(handler) {
  1745. handlers.push(handler);
  1746. return () => {
  1747. const i = handlers.indexOf(handler);
  1748. if (i > -1)
  1749. handlers.splice(i, 1);
  1750. };
  1751. }
  1752. function reset() {
  1753. handlers = [];
  1754. }
  1755. return {
  1756. add,
  1757. list: () => handlers.slice(),
  1758. reset,
  1759. };
  1760. }
  1761. function registerGuard(record, name, guard) {
  1762. const removeFromList = () => {
  1763. record[name].delete(guard);
  1764. };
  1765. vue.onUnmounted(removeFromList);
  1766. vue.onDeactivated(removeFromList);
  1767. vue.onActivated(() => {
  1768. record[name].add(guard);
  1769. });
  1770. record[name].add(guard);
  1771. }
  1772. /**
  1773. * Add a navigation guard that triggers whenever the component for the current
  1774. * location is about to be left. Similar to {@link beforeRouteLeave} but can be
  1775. * used in any component. The guard is removed when the component is unmounted.
  1776. *
  1777. * @param leaveGuard - {@link NavigationGuard}
  1778. */
  1779. function onBeforeRouteLeave(leaveGuard) {
  1780. const activeRecord = vue.inject(matchedRouteKey,
  1781. // to avoid warning
  1782. {}).value;
  1783. if (!activeRecord) {
  1784. return;
  1785. }
  1786. registerGuard(activeRecord, 'leaveGuards', leaveGuard);
  1787. }
  1788. /**
  1789. * Add a navigation guard that triggers whenever the current location is about
  1790. * to be updated. Similar to {@link beforeRouteUpdate} but can be used in any
  1791. * component. The guard is removed when the component is unmounted.
  1792. *
  1793. * @param updateGuard - {@link NavigationGuard}
  1794. */
  1795. function onBeforeRouteUpdate(updateGuard) {
  1796. const activeRecord = vue.inject(matchedRouteKey,
  1797. // to avoid warning
  1798. {}).value;
  1799. if (!activeRecord) {
  1800. return;
  1801. }
  1802. registerGuard(activeRecord, 'updateGuards', updateGuard);
  1803. }
  1804. function guardToPromiseFn(guard, to, from, record, name, runWithContext = fn => fn()) {
  1805. // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
  1806. const enterCallbackArray = record &&
  1807. // name is defined if record is because of the function overload
  1808. (record.enterCallbacks[name] = record.enterCallbacks[name] || []);
  1809. return () => new Promise((resolve, reject) => {
  1810. const next = (valid) => {
  1811. if (valid === false) {
  1812. reject(createRouterError(4 /* ErrorTypes.NAVIGATION_ABORTED */, {
  1813. from,
  1814. to,
  1815. }));
  1816. }
  1817. else if (valid instanceof Error) {
  1818. reject(valid);
  1819. }
  1820. else if (isRouteLocation(valid)) {
  1821. reject(createRouterError(2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */, {
  1822. from: to,
  1823. to: valid,
  1824. }));
  1825. }
  1826. else {
  1827. if (enterCallbackArray &&
  1828. // since enterCallbackArray is truthy, both record and name also are
  1829. record.enterCallbacks[name] === enterCallbackArray &&
  1830. typeof valid === 'function') {
  1831. enterCallbackArray.push(valid);
  1832. }
  1833. resolve();
  1834. }
  1835. };
  1836. // wrapping with Promise.resolve allows it to work with both async and sync guards
  1837. const guardReturn = runWithContext(() => guard.call(record && record.instances[name], to, from, next));
  1838. let guardCall = Promise.resolve(guardReturn);
  1839. if (guard.length < 3)
  1840. guardCall = guardCall.then(next);
  1841. guardCall.catch(err => reject(err));
  1842. });
  1843. }
  1844. function extractComponentsGuards(matched, guardType, to, from, runWithContext = fn => fn()) {
  1845. const guards = [];
  1846. for (const record of matched) {
  1847. for (const name in record.components) {
  1848. let rawComponent = record.components[name];
  1849. // skip update and leave guards if the route component is not mounted
  1850. if (guardType !== 'beforeRouteEnter' && !record.instances[name])
  1851. continue;
  1852. if (isRouteComponent(rawComponent)) {
  1853. // __vccOpts is added by vue-class-component and contain the regular options
  1854. const options = rawComponent.__vccOpts || rawComponent;
  1855. const guard = options[guardType];
  1856. guard &&
  1857. guards.push(guardToPromiseFn(guard, to, from, record, name, runWithContext));
  1858. }
  1859. else {
  1860. // start requesting the chunk already
  1861. let componentPromise = rawComponent();
  1862. guards.push(() => componentPromise.then(resolved => {
  1863. if (!resolved)
  1864. return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}"`));
  1865. const resolvedComponent = isESModule(resolved)
  1866. ? resolved.default
  1867. : resolved;
  1868. // replace the function with the resolved component
  1869. // cannot be null or undefined because we went into the for loop
  1870. record.components[name] = resolvedComponent;
  1871. // __vccOpts is added by vue-class-component and contain the regular options
  1872. const options = resolvedComponent.__vccOpts || resolvedComponent;
  1873. const guard = options[guardType];
  1874. return (guard &&
  1875. guardToPromiseFn(guard, to, from, record, name, runWithContext)());
  1876. }));
  1877. }
  1878. }
  1879. }
  1880. return guards;
  1881. }
  1882. /**
  1883. * Allows differentiating lazy components from functional components and vue-class-component
  1884. * @internal
  1885. *
  1886. * @param component
  1887. */
  1888. function isRouteComponent(component) {
  1889. return (typeof component === 'object' ||
  1890. 'displayName' in component ||
  1891. 'props' in component ||
  1892. '__vccOpts' in component);
  1893. }
  1894. /**
  1895. * Ensures a route is loaded, so it can be passed as o prop to `<RouterView>`.
  1896. *
  1897. * @param route - resolved route to load
  1898. */
  1899. function loadRouteLocation(route) {
  1900. return route.matched.every(record => record.redirect)
  1901. ? Promise.reject(new Error('Cannot load a route that redirects.'))
  1902. : Promise.all(route.matched.map(record => record.components &&
  1903. Promise.all(Object.keys(record.components).reduce((promises, name) => {
  1904. const rawComponent = record.components[name];
  1905. if (typeof rawComponent === 'function' &&
  1906. !('displayName' in rawComponent)) {
  1907. promises.push(rawComponent().then(resolved => {
  1908. if (!resolved)
  1909. return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}". Ensure you passed a function that returns a promise.`));
  1910. const resolvedComponent = isESModule(resolved)
  1911. ? resolved.default
  1912. : resolved;
  1913. // replace the function with the resolved component
  1914. // cannot be null or undefined because we went into the for loop
  1915. record.components[name] = resolvedComponent;
  1916. return;
  1917. }));
  1918. }
  1919. return promises;
  1920. }, [])))).then(() => route);
  1921. }
  1922. // TODO: we could allow currentRoute as a prop to expose `isActive` and
  1923. // `isExactActive` behavior should go through an RFC
  1924. function useLink(props) {
  1925. const router = vue.inject(routerKey);
  1926. const currentRoute = vue.inject(routeLocationKey);
  1927. const route = vue.computed(() => {
  1928. const to = vue.unref(props.to);
  1929. return router.resolve(to);
  1930. });
  1931. const activeRecordIndex = vue.computed(() => {
  1932. const { matched } = route.value;
  1933. const { length } = matched;
  1934. const routeMatched = matched[length - 1];
  1935. const currentMatched = currentRoute.matched;
  1936. if (!routeMatched || !currentMatched.length)
  1937. return -1;
  1938. const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
  1939. if (index > -1)
  1940. return index;
  1941. // possible parent record
  1942. const parentRecordPath = getOriginalPath(matched[length - 2]);
  1943. return (
  1944. // we are dealing with nested routes
  1945. length > 1 &&
  1946. // if the parent and matched route have the same path, this link is
  1947. // referring to the empty child. Or we currently are on a different
  1948. // child of the same parent
  1949. getOriginalPath(routeMatched) === parentRecordPath &&
  1950. // avoid comparing the child with its parent
  1951. currentMatched[currentMatched.length - 1].path !== parentRecordPath
  1952. ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
  1953. : index);
  1954. });
  1955. const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
  1956. includesParams(currentRoute.params, route.value.params));
  1957. const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
  1958. activeRecordIndex.value === currentRoute.matched.length - 1 &&
  1959. isSameRouteLocationParams(currentRoute.params, route.value.params));
  1960. function navigate(e = {}) {
  1961. if (guardEvent(e)) {
  1962. return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to)
  1963. // avoid uncaught errors are they are logged anyway
  1964. ).catch(noop);
  1965. }
  1966. return Promise.resolve();
  1967. }
  1968. /**
  1969. * NOTE: update {@link _RouterLinkI}'s `$slots` type when updating this
  1970. */
  1971. return {
  1972. route,
  1973. href: vue.computed(() => route.value.href),
  1974. isActive,
  1975. isExactActive,
  1976. navigate,
  1977. };
  1978. }
  1979. const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({
  1980. name: 'RouterLink',
  1981. compatConfig: { MODE: 3 },
  1982. props: {
  1983. to: {
  1984. type: [String, Object],
  1985. required: true,
  1986. },
  1987. replace: Boolean,
  1988. activeClass: String,
  1989. // inactiveClass: String,
  1990. exactActiveClass: String,
  1991. custom: Boolean,
  1992. ariaCurrentValue: {
  1993. type: String,
  1994. default: 'page',
  1995. },
  1996. },
  1997. useLink,
  1998. setup(props, { slots }) {
  1999. const link = vue.reactive(useLink(props));
  2000. const { options } = vue.inject(routerKey);
  2001. const elClass = vue.computed(() => ({
  2002. [getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive,
  2003. // [getLinkClass(
  2004. // props.inactiveClass,
  2005. // options.linkInactiveClass,
  2006. // 'router-link-inactive'
  2007. // )]: !link.isExactActive,
  2008. [getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive,
  2009. }));
  2010. return () => {
  2011. const children = slots.default && slots.default(link);
  2012. return props.custom
  2013. ? children
  2014. : vue.h('a', {
  2015. 'aria-current': link.isExactActive
  2016. ? props.ariaCurrentValue
  2017. : null,
  2018. href: link.href,
  2019. // this would override user added attrs but Vue will still add
  2020. // the listener, so we end up triggering both
  2021. onClick: link.navigate,
  2022. class: elClass.value,
  2023. }, children);
  2024. };
  2025. },
  2026. });
  2027. // export the public type for h/tsx inference
  2028. // also to avoid inline import() in generated d.ts files
  2029. /**
  2030. * Component to render a link that triggers a navigation on click.
  2031. */
  2032. const RouterLink = RouterLinkImpl;
  2033. function guardEvent(e) {
  2034. // don't redirect with control keys
  2035. if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
  2036. return;
  2037. // don't redirect when preventDefault called
  2038. if (e.defaultPrevented)
  2039. return;
  2040. // don't redirect on right click
  2041. if (e.button !== undefined && e.button !== 0)
  2042. return;
  2043. // don't redirect if `target="_blank"`
  2044. // @ts-expect-error getAttribute does exist
  2045. if (e.currentTarget && e.currentTarget.getAttribute) {
  2046. // @ts-expect-error getAttribute exists
  2047. const target = e.currentTarget.getAttribute('target');
  2048. if (/\b_blank\b/i.test(target))
  2049. return;
  2050. }
  2051. // this may be a Weex event which doesn't have this method
  2052. if (e.preventDefault)
  2053. e.preventDefault();
  2054. return true;
  2055. }
  2056. function includesParams(outer, inner) {
  2057. for (const key in inner) {
  2058. const innerValue = inner[key];
  2059. const outerValue = outer[key];
  2060. if (typeof innerValue === 'string') {
  2061. if (innerValue !== outerValue)
  2062. return false;
  2063. }
  2064. else {
  2065. if (!isArray(outerValue) ||
  2066. outerValue.length !== innerValue.length ||
  2067. innerValue.some((value, i) => value !== outerValue[i]))
  2068. return false;
  2069. }
  2070. }
  2071. return true;
  2072. }
  2073. /**
  2074. * Get the original path value of a record by following its aliasOf
  2075. * @param record
  2076. */
  2077. function getOriginalPath(record) {
  2078. return record ? (record.aliasOf ? record.aliasOf.path : record.path) : '';
  2079. }
  2080. /**
  2081. * Utility class to get the active class based on defaults.
  2082. * @param propClass
  2083. * @param globalClass
  2084. * @param defaultClass
  2085. */
  2086. const getLinkClass = (propClass, globalClass, defaultClass) => propClass != null
  2087. ? propClass
  2088. : globalClass != null
  2089. ? globalClass
  2090. : defaultClass;
  2091. const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({
  2092. name: 'RouterView',
  2093. // #674 we manually inherit them
  2094. inheritAttrs: false,
  2095. props: {
  2096. name: {
  2097. type: String,
  2098. default: 'default',
  2099. },
  2100. route: Object,
  2101. },
  2102. // Better compat for @vue/compat users
  2103. // https://github.com/vuejs/router/issues/1315
  2104. compatConfig: { MODE: 3 },
  2105. setup(props, { attrs, slots }) {
  2106. const injectedRoute = vue.inject(routerViewLocationKey);
  2107. const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
  2108. const injectedDepth = vue.inject(viewDepthKey, 0);
  2109. // The depth changes based on empty components option, which allows passthrough routes e.g. routes with children
  2110. // that are used to reuse the `path` property
  2111. const depth = vue.computed(() => {
  2112. let initialDepth = vue.unref(injectedDepth);
  2113. const { matched } = routeToDisplay.value;
  2114. let matchedRoute;
  2115. while ((matchedRoute = matched[initialDepth]) &&
  2116. !matchedRoute.components) {
  2117. initialDepth++;
  2118. }
  2119. return initialDepth;
  2120. });
  2121. const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
  2122. vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
  2123. vue.provide(matchedRouteKey, matchedRouteRef);
  2124. vue.provide(routerViewLocationKey, routeToDisplay);
  2125. const viewRef = vue.ref();
  2126. // watch at the same time the component instance, the route record we are
  2127. // rendering, and the name
  2128. vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {
  2129. // copy reused instances
  2130. if (to) {
  2131. // this will update the instance for new instances as well as reused
  2132. // instances when navigating to a new route
  2133. to.instances[name] = instance;
  2134. // the component instance is reused for a different route or name, so
  2135. // we copy any saved update or leave guards. With async setup, the
  2136. // mounting component will mount before the matchedRoute changes,
  2137. // making instance === oldInstance, so we check if guards have been
  2138. // added before. This works because we remove guards when
  2139. // unmounting/deactivating components
  2140. if (from && from !== to && instance && instance === oldInstance) {
  2141. if (!to.leaveGuards.size) {
  2142. to.leaveGuards = from.leaveGuards;
  2143. }
  2144. if (!to.updateGuards.size) {
  2145. to.updateGuards = from.updateGuards;
  2146. }
  2147. }
  2148. }
  2149. // trigger beforeRouteEnter next callbacks
  2150. if (instance &&
  2151. to &&
  2152. // if there is no instance but to and from are the same this might be
  2153. // the first visit
  2154. (!from || !isSameRouteRecord(to, from) || !oldInstance)) {
  2155. (to.enterCallbacks[name] || []).forEach(callback => callback(instance));
  2156. }
  2157. }, { flush: 'post' });
  2158. return () => {
  2159. const route = routeToDisplay.value;
  2160. // we need the value at the time we render because when we unmount, we
  2161. // navigated to a different location so the value is different
  2162. const currentName = props.name;
  2163. const matchedRoute = matchedRouteRef.value;
  2164. const ViewComponent = matchedRoute && matchedRoute.components[currentName];
  2165. if (!ViewComponent) {
  2166. return normalizeSlot(slots.default, { Component: ViewComponent, route });
  2167. }
  2168. // props from route configuration
  2169. const routePropsOption = matchedRoute.props[currentName];
  2170. const routeProps = routePropsOption
  2171. ? routePropsOption === true
  2172. ? route.params
  2173. : typeof routePropsOption === 'function'
  2174. ? routePropsOption(route)
  2175. : routePropsOption
  2176. : null;
  2177. const onVnodeUnmounted = vnode => {
  2178. // remove the instance reference to prevent leak
  2179. if (vnode.component.isUnmounted) {
  2180. matchedRoute.instances[currentName] = null;
  2181. }
  2182. };
  2183. const component = vue.h(ViewComponent, assign({}, routeProps, attrs, {
  2184. onVnodeUnmounted,
  2185. ref: viewRef,
  2186. }));
  2187. return (
  2188. // pass the vnode to the slot as a prop.
  2189. // h and <component :is="..."> both accept vnodes
  2190. normalizeSlot(slots.default, { Component: component, route }) ||
  2191. component);
  2192. };
  2193. },
  2194. });
  2195. function normalizeSlot(slot, data) {
  2196. if (!slot)
  2197. return null;
  2198. const slotContent = slot(data);
  2199. return slotContent.length === 1 ? slotContent[0] : slotContent;
  2200. }
  2201. // export the public type for h/tsx inference
  2202. // also to avoid inline import() in generated d.ts files
  2203. /**
  2204. * Component to display the current route the user is at.
  2205. */
  2206. const RouterView = RouterViewImpl;
  2207. /**
  2208. * Creates a Router instance that can be used by a Vue app.
  2209. *
  2210. * @param options - {@link RouterOptions}
  2211. */
  2212. function createRouter(options) {
  2213. const matcher = createRouterMatcher(options.routes, options);
  2214. const parseQuery$1 = options.parseQuery || parseQuery;
  2215. const stringifyQuery$1 = options.stringifyQuery || stringifyQuery;
  2216. const routerHistory = options.history;
  2217. const beforeGuards = useCallbacks();
  2218. const beforeResolveGuards = useCallbacks();
  2219. const afterGuards = useCallbacks();
  2220. const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
  2221. let pendingLocation = START_LOCATION_NORMALIZED;
  2222. // leave the scrollRestoration if no scrollBehavior is provided
  2223. if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
  2224. history.scrollRestoration = 'manual';
  2225. }
  2226. const normalizeParams = applyToParams.bind(null, paramValue => '' + paramValue);
  2227. const encodeParams = applyToParams.bind(null, encodeParam);
  2228. const decodeParams =
  2229. // @ts-expect-error: intentionally avoid the type check
  2230. applyToParams.bind(null, decode);
  2231. function addRoute(parentOrRoute, route) {
  2232. let parent;
  2233. let record;
  2234. if (isRouteName(parentOrRoute)) {
  2235. parent = matcher.getRecordMatcher(parentOrRoute);
  2236. record = route;
  2237. }
  2238. else {
  2239. record = parentOrRoute;
  2240. }
  2241. return matcher.addRoute(record, parent);
  2242. }
  2243. function removeRoute(name) {
  2244. const recordMatcher = matcher.getRecordMatcher(name);
  2245. if (recordMatcher) {
  2246. matcher.removeRoute(recordMatcher);
  2247. }
  2248. }
  2249. function getRoutes() {
  2250. return matcher.getRoutes().map(routeMatcher => routeMatcher.record);
  2251. }
  2252. function hasRoute(name) {
  2253. return !!matcher.getRecordMatcher(name);
  2254. }
  2255. function resolve(rawLocation, currentLocation) {
  2256. // const objectLocation = routerLocationAsObject(rawLocation)
  2257. // we create a copy to modify it later
  2258. currentLocation = assign({}, currentLocation || currentRoute.value);
  2259. if (typeof rawLocation === 'string') {
  2260. const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
  2261. const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
  2262. const href = routerHistory.createHref(locationNormalized.fullPath);
  2263. // locationNormalized is always a new object
  2264. return assign(locationNormalized, matchedRoute, {
  2265. params: decodeParams(matchedRoute.params),
  2266. hash: decode(locationNormalized.hash),
  2267. redirectedFrom: undefined,
  2268. href,
  2269. });
  2270. }
  2271. let matcherLocation;
  2272. // path could be relative in object as well
  2273. if (rawLocation.path != null) {
  2274. matcherLocation = assign({}, rawLocation, {
  2275. path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
  2276. });
  2277. }
  2278. else {
  2279. // remove any nullish param
  2280. const targetParams = assign({}, rawLocation.params);
  2281. for (const key in targetParams) {
  2282. if (targetParams[key] == null) {
  2283. delete targetParams[key];
  2284. }
  2285. }
  2286. // pass encoded values to the matcher, so it can produce encoded path and fullPath
  2287. matcherLocation = assign({}, rawLocation, {
  2288. params: encodeParams(targetParams),
  2289. });
  2290. // current location params are decoded, we need to encode them in case the
  2291. // matcher merges the params
  2292. currentLocation.params = encodeParams(currentLocation.params);
  2293. }
  2294. const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
  2295. const hash = rawLocation.hash || '';
  2296. // the matcher might have merged current location params, so
  2297. // we need to run the decoding again
  2298. matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));
  2299. const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {
  2300. hash: encodeHash(hash),
  2301. path: matchedRoute.path,
  2302. }));
  2303. const href = routerHistory.createHref(fullPath);
  2304. return assign({
  2305. fullPath,
  2306. // keep the hash encoded so fullPath is effectively path + encodedQuery +
  2307. // hash
  2308. hash,
  2309. query:
  2310. // if the user is using a custom query lib like qs, we might have
  2311. // nested objects, so we keep the query as is, meaning it can contain
  2312. // numbers at `$route.query`, but at the point, the user will have to
  2313. // use their own type anyway.
  2314. // https://github.com/vuejs/router/issues/328#issuecomment-649481567
  2315. stringifyQuery$1 === stringifyQuery
  2316. ? normalizeQuery(rawLocation.query)
  2317. : (rawLocation.query || {}),
  2318. }, matchedRoute, {
  2319. redirectedFrom: undefined,
  2320. href,
  2321. });
  2322. }
  2323. function locationAsObject(to) {
  2324. return typeof to === 'string'
  2325. ? parseURL(parseQuery$1, to, currentRoute.value.path)
  2326. : assign({}, to);
  2327. }
  2328. function checkCanceledNavigation(to, from) {
  2329. if (pendingLocation !== to) {
  2330. return createRouterError(8 /* ErrorTypes.NAVIGATION_CANCELLED */, {
  2331. from,
  2332. to,
  2333. });
  2334. }
  2335. }
  2336. function push(to) {
  2337. return pushWithRedirect(to);
  2338. }
  2339. function replace(to) {
  2340. return push(assign(locationAsObject(to), { replace: true }));
  2341. }
  2342. function handleRedirectRecord(to) {
  2343. const lastMatched = to.matched[to.matched.length - 1];
  2344. if (lastMatched && lastMatched.redirect) {
  2345. const { redirect } = lastMatched;
  2346. let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect;
  2347. if (typeof newTargetLocation === 'string') {
  2348. newTargetLocation =
  2349. newTargetLocation.includes('?') || newTargetLocation.includes('#')
  2350. ? (newTargetLocation = locationAsObject(newTargetLocation))
  2351. : // force empty params
  2352. { path: newTargetLocation };
  2353. // @ts-expect-error: force empty params when a string is passed to let
  2354. // the router parse them again
  2355. newTargetLocation.params = {};
  2356. }
  2357. return assign({
  2358. query: to.query,
  2359. hash: to.hash,
  2360. // avoid transferring params if the redirect has a path
  2361. params: newTargetLocation.path != null ? {} : to.params,
  2362. }, newTargetLocation);
  2363. }
  2364. }
  2365. function pushWithRedirect(to, redirectedFrom) {
  2366. const targetLocation = (pendingLocation = resolve(to));
  2367. const from = currentRoute.value;
  2368. const data = to.state;
  2369. const force = to.force;
  2370. // to could be a string where `replace` is a function
  2371. const replace = to.replace === true;
  2372. const shouldRedirect = handleRedirectRecord(targetLocation);
  2373. if (shouldRedirect)
  2374. return pushWithRedirect(assign(locationAsObject(shouldRedirect), {
  2375. state: typeof shouldRedirect === 'object'
  2376. ? assign({}, data, shouldRedirect.state)
  2377. : data,
  2378. force,
  2379. replace,
  2380. }),
  2381. // keep original redirectedFrom if it exists
  2382. redirectedFrom || targetLocation);
  2383. // if it was a redirect we already called `pushWithRedirect` above
  2384. const toLocation = targetLocation;
  2385. toLocation.redirectedFrom = redirectedFrom;
  2386. let failure;
  2387. if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {
  2388. failure = createRouterError(16 /* ErrorTypes.NAVIGATION_DUPLICATED */, { to: toLocation, from });
  2389. // trigger scroll to allow scrolling to the same anchor
  2390. handleScroll(from, from,
  2391. // this is a push, the only way for it to be triggered from a
  2392. // history.listen is with a redirect, which makes it become a push
  2393. true,
  2394. // This cannot be the first navigation because the initial location
  2395. // cannot be manually navigated to
  2396. false);
  2397. }
  2398. return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
  2399. .catch((error) => isNavigationFailure(error)
  2400. ? // navigation redirects still mark the router as ready
  2401. isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)
  2402. ? error
  2403. : markAsReady(error) // also returns the error
  2404. : // reject any unknown error
  2405. triggerError(error, toLocation, from))
  2406. .then((failure) => {
  2407. if (failure) {
  2408. if (isNavigationFailure(failure, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {
  2409. return pushWithRedirect(
  2410. // keep options
  2411. assign({
  2412. // preserve an existing replacement but allow the redirect to override it
  2413. replace,
  2414. }, locationAsObject(failure.to), {
  2415. state: typeof failure.to === 'object'
  2416. ? assign({}, data, failure.to.state)
  2417. : data,
  2418. force,
  2419. }),
  2420. // preserve the original redirectedFrom if any
  2421. redirectedFrom || toLocation);
  2422. }
  2423. }
  2424. else {
  2425. // if we fail we don't finalize the navigation
  2426. failure = finalizeNavigation(toLocation, from, true, replace, data);
  2427. }
  2428. triggerAfterEach(toLocation, from, failure);
  2429. return failure;
  2430. });
  2431. }
  2432. /**
  2433. * Helper to reject and skip all navigation guards if a new navigation happened
  2434. * @param to
  2435. * @param from
  2436. */
  2437. function checkCanceledNavigationAndReject(to, from) {
  2438. const error = checkCanceledNavigation(to, from);
  2439. return error ? Promise.reject(error) : Promise.resolve();
  2440. }
  2441. function runWithContext(fn) {
  2442. const app = installedApps.values().next().value;
  2443. // support Vue < 3.3
  2444. return app && typeof app.runWithContext === 'function'
  2445. ? app.runWithContext(fn)
  2446. : fn();
  2447. }
  2448. // TODO: refactor the whole before guards by internally using router.beforeEach
  2449. function navigate(to, from) {
  2450. let guards;
  2451. const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from);
  2452. // all components here have been resolved once because we are leaving
  2453. guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from);
  2454. // leavingRecords is already reversed
  2455. for (const record of leavingRecords) {
  2456. record.leaveGuards.forEach(guard => {
  2457. guards.push(guardToPromiseFn(guard, to, from));
  2458. });
  2459. }
  2460. const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from);
  2461. guards.push(canceledNavigationCheck);
  2462. // run the queue of per route beforeRouteLeave guards
  2463. return (runGuardQueue(guards)
  2464. .then(() => {
  2465. // check global guards beforeEach
  2466. guards = [];
  2467. for (const guard of beforeGuards.list()) {
  2468. guards.push(guardToPromiseFn(guard, to, from));
  2469. }
  2470. guards.push(canceledNavigationCheck);
  2471. return runGuardQueue(guards);
  2472. })
  2473. .then(() => {
  2474. // check in components beforeRouteUpdate
  2475. guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from);
  2476. for (const record of updatingRecords) {
  2477. record.updateGuards.forEach(guard => {
  2478. guards.push(guardToPromiseFn(guard, to, from));
  2479. });
  2480. }
  2481. guards.push(canceledNavigationCheck);
  2482. // run the queue of per route beforeEnter guards
  2483. return runGuardQueue(guards);
  2484. })
  2485. .then(() => {
  2486. // check the route beforeEnter
  2487. guards = [];
  2488. for (const record of enteringRecords) {
  2489. // do not trigger beforeEnter on reused views
  2490. if (record.beforeEnter) {
  2491. if (isArray(record.beforeEnter)) {
  2492. for (const beforeEnter of record.beforeEnter)
  2493. guards.push(guardToPromiseFn(beforeEnter, to, from));
  2494. }
  2495. else {
  2496. guards.push(guardToPromiseFn(record.beforeEnter, to, from));
  2497. }
  2498. }
  2499. }
  2500. guards.push(canceledNavigationCheck);
  2501. // run the queue of per route beforeEnter guards
  2502. return runGuardQueue(guards);
  2503. })
  2504. .then(() => {
  2505. // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
  2506. // clear existing enterCallbacks, these are added by extractComponentsGuards
  2507. to.matched.forEach(record => (record.enterCallbacks = {}));
  2508. // check in-component beforeRouteEnter
  2509. guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from, runWithContext);
  2510. guards.push(canceledNavigationCheck);
  2511. // run the queue of per route beforeEnter guards
  2512. return runGuardQueue(guards);
  2513. })
  2514. .then(() => {
  2515. // check global guards beforeResolve
  2516. guards = [];
  2517. for (const guard of beforeResolveGuards.list()) {
  2518. guards.push(guardToPromiseFn(guard, to, from));
  2519. }
  2520. guards.push(canceledNavigationCheck);
  2521. return runGuardQueue(guards);
  2522. })
  2523. // catch any navigation canceled
  2524. .catch(err => isNavigationFailure(err, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)
  2525. ? err
  2526. : Promise.reject(err)));
  2527. }
  2528. function triggerAfterEach(to, from, failure) {
  2529. // navigation is confirmed, call afterGuards
  2530. // TODO: wrap with error handlers
  2531. afterGuards
  2532. .list()
  2533. .forEach(guard => runWithContext(() => guard(to, from, failure)));
  2534. }
  2535. /**
  2536. * - Cleans up any navigation guards
  2537. * - Changes the url if necessary
  2538. * - Calls the scrollBehavior
  2539. */
  2540. function finalizeNavigation(toLocation, from, isPush, replace, data) {
  2541. // a more recent navigation took place
  2542. const error = checkCanceledNavigation(toLocation, from);
  2543. if (error)
  2544. return error;
  2545. // only consider as push if it's not the first navigation
  2546. const isFirstNavigation = from === START_LOCATION_NORMALIZED;
  2547. const state = !isBrowser ? {} : history.state;
  2548. // change URL only if the user did a push/replace and if it's not the initial navigation because
  2549. // it's just reflecting the url
  2550. if (isPush) {
  2551. // on the initial navigation, we want to reuse the scroll position from
  2552. // history state if it exists
  2553. if (replace || isFirstNavigation)
  2554. routerHistory.replace(toLocation.fullPath, assign({
  2555. scroll: isFirstNavigation && state && state.scroll,
  2556. }, data));
  2557. else
  2558. routerHistory.push(toLocation.fullPath, data);
  2559. }
  2560. // accept current navigation
  2561. currentRoute.value = toLocation;
  2562. handleScroll(toLocation, from, isPush, isFirstNavigation);
  2563. markAsReady();
  2564. }
  2565. let removeHistoryListener;
  2566. // attach listener to history to trigger navigations
  2567. function setupListeners() {
  2568. // avoid setting up listeners twice due to an invalid first navigation
  2569. if (removeHistoryListener)
  2570. return;
  2571. removeHistoryListener = routerHistory.listen((to, _from, info) => {
  2572. if (!router.listening)
  2573. return;
  2574. // cannot be a redirect route because it was in history
  2575. const toLocation = resolve(to);
  2576. // due to dynamic routing, and to hash history with manual navigation
  2577. // (manually changing the url or calling history.hash = '#/somewhere'),
  2578. // there could be a redirect record in history
  2579. const shouldRedirect = handleRedirectRecord(toLocation);
  2580. if (shouldRedirect) {
  2581. pushWithRedirect(assign(shouldRedirect, { replace: true }), toLocation).catch(noop);
  2582. return;
  2583. }
  2584. pendingLocation = toLocation;
  2585. const from = currentRoute.value;
  2586. // TODO: should be moved to web history?
  2587. if (isBrowser) {
  2588. saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition());
  2589. }
  2590. navigate(toLocation, from)
  2591. .catch((error) => {
  2592. if (isNavigationFailure(error, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {
  2593. return error;
  2594. }
  2595. if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {
  2596. // Here we could call if (info.delta) routerHistory.go(-info.delta,
  2597. // false) but this is bug prone as we have no way to wait the
  2598. // navigation to be finished before calling pushWithRedirect. Using
  2599. // a setTimeout of 16ms seems to work but there is no guarantee for
  2600. // it to work on every browser. So instead we do not restore the
  2601. // history entry and trigger a new navigation as requested by the
  2602. // navigation guard.
  2603. // the error is already handled by router.push we just want to avoid
  2604. // logging the error
  2605. pushWithRedirect(error.to, toLocation
  2606. // avoid an uncaught rejection, let push call triggerError
  2607. )
  2608. .then(failure => {
  2609. // manual change in hash history #916 ending up in the URL not
  2610. // changing, but it was changed by the manual url change, so we
  2611. // need to manually change it ourselves
  2612. if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
  2613. 16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&
  2614. !info.delta &&
  2615. info.type === NavigationType.pop) {
  2616. routerHistory.go(-1, false);
  2617. }
  2618. })
  2619. .catch(noop);
  2620. // avoid the then branch
  2621. return Promise.reject();
  2622. }
  2623. // do not restore history on unknown direction
  2624. if (info.delta) {
  2625. routerHistory.go(-info.delta, false);
  2626. }
  2627. // unrecognized error, transfer to the global handler
  2628. return triggerError(error, toLocation, from);
  2629. })
  2630. .then((failure) => {
  2631. failure =
  2632. failure ||
  2633. finalizeNavigation(
  2634. // after navigation, all matched components are resolved
  2635. toLocation, from, false);
  2636. // revert the navigation
  2637. if (failure) {
  2638. if (info.delta &&
  2639. // a new navigation has been triggered, so we do not want to revert, that will change the current history
  2640. // entry while a different route is displayed
  2641. !isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {
  2642. routerHistory.go(-info.delta, false);
  2643. }
  2644. else if (info.type === NavigationType.pop &&
  2645. isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {
  2646. // manual change in hash history #916
  2647. // it's like a push but lacks the information of the direction
  2648. routerHistory.go(-1, false);
  2649. }
  2650. }
  2651. triggerAfterEach(toLocation, from, failure);
  2652. })
  2653. // avoid warnings in the console about uncaught rejections, they are logged by triggerErrors
  2654. .catch(noop);
  2655. });
  2656. }
  2657. // Initialization and Errors
  2658. let readyHandlers = useCallbacks();
  2659. let errorListeners = useCallbacks();
  2660. let ready;
  2661. /**
  2662. * Trigger errorListeners added via onError and throws the error as well
  2663. *
  2664. * @param error - error to throw
  2665. * @param to - location we were navigating to when the error happened
  2666. * @param from - location we were navigating from when the error happened
  2667. * @returns the error as a rejected promise
  2668. */
  2669. function triggerError(error, to, from) {
  2670. markAsReady(error);
  2671. const list = errorListeners.list();
  2672. if (list.length) {
  2673. list.forEach(handler => handler(error, to, from));
  2674. }
  2675. else {
  2676. console.error(error);
  2677. }
  2678. // reject the error no matter there were error listeners or not
  2679. return Promise.reject(error);
  2680. }
  2681. function isReady() {
  2682. if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
  2683. return Promise.resolve();
  2684. return new Promise((resolve, reject) => {
  2685. readyHandlers.add([resolve, reject]);
  2686. });
  2687. }
  2688. function markAsReady(err) {
  2689. if (!ready) {
  2690. // still not ready if an error happened
  2691. ready = !err;
  2692. setupListeners();
  2693. readyHandlers
  2694. .list()
  2695. .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
  2696. readyHandlers.reset();
  2697. }
  2698. return err;
  2699. }
  2700. // Scroll behavior
  2701. function handleScroll(to, from, isPush, isFirstNavigation) {
  2702. const { scrollBehavior } = options;
  2703. if (!isBrowser || !scrollBehavior)
  2704. return Promise.resolve();
  2705. const scrollPosition = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
  2706. ((isFirstNavigation || !isPush) &&
  2707. history.state &&
  2708. history.state.scroll) ||
  2709. null;
  2710. return vue.nextTick()
  2711. .then(() => scrollBehavior(to, from, scrollPosition))
  2712. .then(position => position && scrollToPosition(position))
  2713. .catch(err => triggerError(err, to, from));
  2714. }
  2715. const go = (delta) => routerHistory.go(delta);
  2716. let started;
  2717. const installedApps = new Set();
  2718. const router = {
  2719. currentRoute,
  2720. listening: true,
  2721. addRoute,
  2722. removeRoute,
  2723. hasRoute,
  2724. getRoutes,
  2725. resolve,
  2726. options,
  2727. push,
  2728. replace,
  2729. go,
  2730. back: () => go(-1),
  2731. forward: () => go(1),
  2732. beforeEach: beforeGuards.add,
  2733. beforeResolve: beforeResolveGuards.add,
  2734. afterEach: afterGuards.add,
  2735. onError: errorListeners.add,
  2736. isReady,
  2737. install(app) {
  2738. const router = this;
  2739. app.component('RouterLink', RouterLink);
  2740. app.component('RouterView', RouterView);
  2741. app.config.globalProperties.$router = router;
  2742. Object.defineProperty(app.config.globalProperties, '$route', {
  2743. enumerable: true,
  2744. get: () => vue.unref(currentRoute),
  2745. });
  2746. // this initial navigation is only necessary on client, on server it doesn't
  2747. // make sense because it will create an extra unnecessary navigation and could
  2748. // lead to problems
  2749. if (isBrowser &&
  2750. // used for the initial navigation client side to avoid pushing
  2751. // multiple times when the router is used in multiple apps
  2752. !started &&
  2753. currentRoute.value === START_LOCATION_NORMALIZED) {
  2754. // see above
  2755. started = true;
  2756. push(routerHistory.location).catch(err => {
  2757. });
  2758. }
  2759. const reactiveRoute = {};
  2760. for (const key in START_LOCATION_NORMALIZED) {
  2761. Object.defineProperty(reactiveRoute, key, {
  2762. get: () => currentRoute.value[key],
  2763. enumerable: true,
  2764. });
  2765. }
  2766. app.provide(routerKey, router);
  2767. app.provide(routeLocationKey, vue.shallowReactive(reactiveRoute));
  2768. app.provide(routerViewLocationKey, currentRoute);
  2769. const unmountApp = app.unmount;
  2770. installedApps.add(app);
  2771. app.unmount = function () {
  2772. installedApps.delete(app);
  2773. // the router is not attached to an app anymore
  2774. if (installedApps.size < 1) {
  2775. // invalidate the current navigation
  2776. pendingLocation = START_LOCATION_NORMALIZED;
  2777. removeHistoryListener && removeHistoryListener();
  2778. removeHistoryListener = null;
  2779. currentRoute.value = START_LOCATION_NORMALIZED;
  2780. started = false;
  2781. ready = false;
  2782. }
  2783. unmountApp();
  2784. };
  2785. },
  2786. };
  2787. // TODO: type this as NavigationGuardReturn or similar instead of any
  2788. function runGuardQueue(guards) {
  2789. return guards.reduce((promise, guard) => promise.then(() => runWithContext(guard)), Promise.resolve());
  2790. }
  2791. return router;
  2792. }
  2793. function extractChangingRecords(to, from) {
  2794. const leavingRecords = [];
  2795. const updatingRecords = [];
  2796. const enteringRecords = [];
  2797. const len = Math.max(from.matched.length, to.matched.length);
  2798. for (let i = 0; i < len; i++) {
  2799. const recordFrom = from.matched[i];
  2800. if (recordFrom) {
  2801. if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
  2802. updatingRecords.push(recordFrom);
  2803. else
  2804. leavingRecords.push(recordFrom);
  2805. }
  2806. const recordTo = to.matched[i];
  2807. if (recordTo) {
  2808. // the type doesn't matter because we are comparing per reference
  2809. if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
  2810. enteringRecords.push(recordTo);
  2811. }
  2812. }
  2813. }
  2814. return [leavingRecords, updatingRecords, enteringRecords];
  2815. }
  2816. /**
  2817. * Returns the router instance. Equivalent to using `$router` inside
  2818. * templates.
  2819. */
  2820. function useRouter() {
  2821. return vue.inject(routerKey);
  2822. }
  2823. /**
  2824. * Returns the current route location. Equivalent to using `$route` inside
  2825. * templates.
  2826. */
  2827. function useRoute() {
  2828. return vue.inject(routeLocationKey);
  2829. }
  2830. exports.RouterLink = RouterLink;
  2831. exports.RouterView = RouterView;
  2832. exports.START_LOCATION = START_LOCATION_NORMALIZED;
  2833. exports.createMemoryHistory = createMemoryHistory;
  2834. exports.createRouter = createRouter;
  2835. exports.createRouterMatcher = createRouterMatcher;
  2836. exports.createWebHashHistory = createWebHashHistory;
  2837. exports.createWebHistory = createWebHistory;
  2838. exports.isNavigationFailure = isNavigationFailure;
  2839. exports.loadRouteLocation = loadRouteLocation;
  2840. exports.matchedRouteKey = matchedRouteKey;
  2841. exports.onBeforeRouteLeave = onBeforeRouteLeave;
  2842. exports.onBeforeRouteUpdate = onBeforeRouteUpdate;
  2843. exports.parseQuery = parseQuery;
  2844. exports.routeLocationKey = routeLocationKey;
  2845. exports.routerKey = routerKey;
  2846. exports.routerViewLocationKey = routerViewLocationKey;
  2847. exports.stringifyQuery = stringifyQuery;
  2848. exports.useLink = useLink;
  2849. exports.useRoute = useRoute;
  2850. exports.useRouter = useRouter;
  2851. exports.viewDepthKey = viewDepthKey;