page.mjs 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  1. var isarray = Array.isArray || function (arr) {
  2. return Object.prototype.toString.call(arr) == '[object Array]';
  3. };
  4. /**
  5. * Expose `pathToRegexp`.
  6. */
  7. var pathToRegexp_1 = pathToRegexp;
  8. var parse_1 = parse;
  9. var compile_1 = compile;
  10. var tokensToFunction_1 = tokensToFunction;
  11. var tokensToRegExp_1 = tokensToRegExp;
  12. /**
  13. * The main path matching regexp utility.
  14. *
  15. * @type {RegExp}
  16. */
  17. var PATH_REGEXP = new RegExp([
  18. // Match escaped characters that would otherwise appear in future matches.
  19. // This allows the user to escape special characters that won't transform.
  20. '(\\\\.)',
  21. // Match Express-style parameters and un-named parameters with a prefix
  22. // and optional suffixes. Matches appear as:
  23. //
  24. // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
  25. // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
  26. // "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
  27. '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))([+*?])?|(\\*))'
  28. ].join('|'), 'g');
  29. /**
  30. * Parse a string for the raw tokens.
  31. *
  32. * @param {String} str
  33. * @return {Array}
  34. */
  35. function parse (str) {
  36. var tokens = [];
  37. var key = 0;
  38. var index = 0;
  39. var path = '';
  40. var res;
  41. while ((res = PATH_REGEXP.exec(str)) != null) {
  42. var m = res[0];
  43. var escaped = res[1];
  44. var offset = res.index;
  45. path += str.slice(index, offset);
  46. index = offset + m.length;
  47. // Ignore already escaped sequences.
  48. if (escaped) {
  49. path += escaped[1];
  50. continue
  51. }
  52. // Push the current path onto the tokens.
  53. if (path) {
  54. tokens.push(path);
  55. path = '';
  56. }
  57. var prefix = res[2];
  58. var name = res[3];
  59. var capture = res[4];
  60. var group = res[5];
  61. var suffix = res[6];
  62. var asterisk = res[7];
  63. var repeat = suffix === '+' || suffix === '*';
  64. var optional = suffix === '?' || suffix === '*';
  65. var delimiter = prefix || '/';
  66. var pattern = capture || group || (asterisk ? '.*' : '[^' + delimiter + ']+?');
  67. tokens.push({
  68. name: name || key++,
  69. prefix: prefix || '',
  70. delimiter: delimiter,
  71. optional: optional,
  72. repeat: repeat,
  73. pattern: escapeGroup(pattern)
  74. });
  75. }
  76. // Match any characters still remaining.
  77. if (index < str.length) {
  78. path += str.substr(index);
  79. }
  80. // If the path exists, push it onto the end.
  81. if (path) {
  82. tokens.push(path);
  83. }
  84. return tokens
  85. }
  86. /**
  87. * Compile a string to a template function for the path.
  88. *
  89. * @param {String} str
  90. * @return {Function}
  91. */
  92. function compile (str) {
  93. return tokensToFunction(parse(str))
  94. }
  95. /**
  96. * Expose a method for transforming tokens into the path function.
  97. */
  98. function tokensToFunction (tokens) {
  99. // Compile all the tokens into regexps.
  100. var matches = new Array(tokens.length);
  101. // Compile all the patterns before compilation.
  102. for (var i = 0; i < tokens.length; i++) {
  103. if (typeof tokens[i] === 'object') {
  104. matches[i] = new RegExp('^' + tokens[i].pattern + '$');
  105. }
  106. }
  107. return function (obj) {
  108. var path = '';
  109. var data = obj || {};
  110. for (var i = 0; i < tokens.length; i++) {
  111. var token = tokens[i];
  112. if (typeof token === 'string') {
  113. path += token;
  114. continue
  115. }
  116. var value = data[token.name];
  117. var segment;
  118. if (value == null) {
  119. if (token.optional) {
  120. continue
  121. } else {
  122. throw new TypeError('Expected "' + token.name + '" to be defined')
  123. }
  124. }
  125. if (isarray(value)) {
  126. if (!token.repeat) {
  127. throw new TypeError('Expected "' + token.name + '" to not repeat, but received "' + value + '"')
  128. }
  129. if (value.length === 0) {
  130. if (token.optional) {
  131. continue
  132. } else {
  133. throw new TypeError('Expected "' + token.name + '" to not be empty')
  134. }
  135. }
  136. for (var j = 0; j < value.length; j++) {
  137. segment = encodeURIComponent(value[j]);
  138. if (!matches[i].test(segment)) {
  139. throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
  140. }
  141. path += (j === 0 ? token.prefix : token.delimiter) + segment;
  142. }
  143. continue
  144. }
  145. segment = encodeURIComponent(value);
  146. if (!matches[i].test(segment)) {
  147. throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
  148. }
  149. path += token.prefix + segment;
  150. }
  151. return path
  152. }
  153. }
  154. /**
  155. * Escape a regular expression string.
  156. *
  157. * @param {String} str
  158. * @return {String}
  159. */
  160. function escapeString (str) {
  161. return str.replace(/([.+*?=^!:${}()[\]|\/])/g, '\\$1')
  162. }
  163. /**
  164. * Escape the capturing group by escaping special characters and meaning.
  165. *
  166. * @param {String} group
  167. * @return {String}
  168. */
  169. function escapeGroup (group) {
  170. return group.replace(/([=!:$\/()])/g, '\\$1')
  171. }
  172. /**
  173. * Attach the keys as a property of the regexp.
  174. *
  175. * @param {RegExp} re
  176. * @param {Array} keys
  177. * @return {RegExp}
  178. */
  179. function attachKeys (re, keys) {
  180. re.keys = keys;
  181. return re
  182. }
  183. /**
  184. * Get the flags for a regexp from the options.
  185. *
  186. * @param {Object} options
  187. * @return {String}
  188. */
  189. function flags (options) {
  190. return options.sensitive ? '' : 'i'
  191. }
  192. /**
  193. * Pull out keys from a regexp.
  194. *
  195. * @param {RegExp} path
  196. * @param {Array} keys
  197. * @return {RegExp}
  198. */
  199. function regexpToRegexp (path, keys) {
  200. // Use a negative lookahead to match only capturing groups.
  201. var groups = path.source.match(/\((?!\?)/g);
  202. if (groups) {
  203. for (var i = 0; i < groups.length; i++) {
  204. keys.push({
  205. name: i,
  206. prefix: null,
  207. delimiter: null,
  208. optional: false,
  209. repeat: false,
  210. pattern: null
  211. });
  212. }
  213. }
  214. return attachKeys(path, keys)
  215. }
  216. /**
  217. * Transform an array into a regexp.
  218. *
  219. * @param {Array} path
  220. * @param {Array} keys
  221. * @param {Object} options
  222. * @return {RegExp}
  223. */
  224. function arrayToRegexp (path, keys, options) {
  225. var parts = [];
  226. for (var i = 0; i < path.length; i++) {
  227. parts.push(pathToRegexp(path[i], keys, options).source);
  228. }
  229. var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options));
  230. return attachKeys(regexp, keys)
  231. }
  232. /**
  233. * Create a path regexp from string input.
  234. *
  235. * @param {String} path
  236. * @param {Array} keys
  237. * @param {Object} options
  238. * @return {RegExp}
  239. */
  240. function stringToRegexp (path, keys, options) {
  241. var tokens = parse(path);
  242. var re = tokensToRegExp(tokens, options);
  243. // Attach keys back to the regexp.
  244. for (var i = 0; i < tokens.length; i++) {
  245. if (typeof tokens[i] !== 'string') {
  246. keys.push(tokens[i]);
  247. }
  248. }
  249. return attachKeys(re, keys)
  250. }
  251. /**
  252. * Expose a function for taking tokens and returning a RegExp.
  253. *
  254. * @param {Array} tokens
  255. * @param {Array} keys
  256. * @param {Object} options
  257. * @return {RegExp}
  258. */
  259. function tokensToRegExp (tokens, options) {
  260. options = options || {};
  261. var strict = options.strict;
  262. var end = options.end !== false;
  263. var route = '';
  264. var lastToken = tokens[tokens.length - 1];
  265. var endsWithSlash = typeof lastToken === 'string' && /\/$/.test(lastToken);
  266. // Iterate over the tokens and create our regexp string.
  267. for (var i = 0; i < tokens.length; i++) {
  268. var token = tokens[i];
  269. if (typeof token === 'string') {
  270. route += escapeString(token);
  271. } else {
  272. var prefix = escapeString(token.prefix);
  273. var capture = token.pattern;
  274. if (token.repeat) {
  275. capture += '(?:' + prefix + capture + ')*';
  276. }
  277. if (token.optional) {
  278. if (prefix) {
  279. capture = '(?:' + prefix + '(' + capture + '))?';
  280. } else {
  281. capture = '(' + capture + ')?';
  282. }
  283. } else {
  284. capture = prefix + '(' + capture + ')';
  285. }
  286. route += capture;
  287. }
  288. }
  289. // In non-strict mode we allow a slash at the end of match. If the path to
  290. // match already ends with a slash, we remove it for consistency. The slash
  291. // is valid at the end of a path match, not in the middle. This is important
  292. // in non-ending mode, where "/test/" shouldn't match "/test//route".
  293. if (!strict) {
  294. route = (endsWithSlash ? route.slice(0, -2) : route) + '(?:\\/(?=$))?';
  295. }
  296. if (end) {
  297. route += '$';
  298. } else {
  299. // In non-ending mode, we need the capturing groups to match as much as
  300. // possible by using a positive lookahead to the end or next path segment.
  301. route += strict && endsWithSlash ? '' : '(?=\\/|$)';
  302. }
  303. return new RegExp('^' + route, flags(options))
  304. }
  305. /**
  306. * Normalize the given path string, returning a regular expression.
  307. *
  308. * An empty array can be passed in for the keys, which will hold the
  309. * placeholder key descriptions. For example, using `/user/:id`, `keys` will
  310. * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
  311. *
  312. * @param {(String|RegExp|Array)} path
  313. * @param {Array} [keys]
  314. * @param {Object} [options]
  315. * @return {RegExp}
  316. */
  317. function pathToRegexp (path, keys, options) {
  318. keys = keys || [];
  319. if (!isarray(keys)) {
  320. options = keys;
  321. keys = [];
  322. } else if (!options) {
  323. options = {};
  324. }
  325. if (path instanceof RegExp) {
  326. return regexpToRegexp(path, keys, options)
  327. }
  328. if (isarray(path)) {
  329. return arrayToRegexp(path, keys, options)
  330. }
  331. return stringToRegexp(path, keys, options)
  332. }
  333. pathToRegexp_1.parse = parse_1;
  334. pathToRegexp_1.compile = compile_1;
  335. pathToRegexp_1.tokensToFunction = tokensToFunction_1;
  336. pathToRegexp_1.tokensToRegExp = tokensToRegExp_1;
  337. /**
  338. * Module dependencies.
  339. */
  340. /**
  341. * Short-cuts for global-object checks
  342. */
  343. var hasDocument = ('undefined' !== typeof document);
  344. var hasWindow = ('undefined' !== typeof window);
  345. var hasHistory = ('undefined' !== typeof history);
  346. var hasProcess = typeof process !== 'undefined';
  347. /**
  348. * Detect click event
  349. */
  350. var clickEvent = hasDocument && document.ontouchstart ? 'touchstart' : 'click';
  351. /**
  352. * To work properly with the URL
  353. * history.location generated polyfill in https://github.com/devote/HTML5-History-API
  354. */
  355. var isLocation = hasWindow && !!(window.history.location || window.location);
  356. /**
  357. * The page instance
  358. * @api private
  359. */
  360. function Page() {
  361. // public things
  362. this.callbacks = [];
  363. this.exits = [];
  364. this.current = '';
  365. this.len = 0;
  366. // private things
  367. this._decodeURLComponents = true;
  368. this._base = '';
  369. this._strict = false;
  370. this._running = false;
  371. this._hashbang = false;
  372. // bound functions
  373. this.clickHandler = this.clickHandler.bind(this);
  374. this._onpopstate = this._onpopstate.bind(this);
  375. }
  376. /**
  377. * Configure the instance of page. This can be called multiple times.
  378. *
  379. * @param {Object} options
  380. * @api public
  381. */
  382. Page.prototype.configure = function(options) {
  383. var opts = options || {};
  384. this._window = opts.window || (hasWindow && window);
  385. this._decodeURLComponents = opts.decodeURLComponents !== false;
  386. this._popstate = opts.popstate !== false && hasWindow;
  387. this._click = opts.click !== false && hasDocument;
  388. this._hashbang = !!opts.hashbang;
  389. var _window = this._window;
  390. if(this._popstate) {
  391. _window.addEventListener('popstate', this._onpopstate, false);
  392. } else if(hasWindow) {
  393. _window.removeEventListener('popstate', this._onpopstate, false);
  394. }
  395. if (this._click) {
  396. _window.document.addEventListener(clickEvent, this.clickHandler, false);
  397. } else if(hasDocument) {
  398. _window.document.removeEventListener(clickEvent, this.clickHandler, false);
  399. }
  400. if(this._hashbang && hasWindow && !hasHistory) {
  401. _window.addEventListener('hashchange', this._onpopstate, false);
  402. } else if(hasWindow) {
  403. _window.removeEventListener('hashchange', this._onpopstate, false);
  404. }
  405. };
  406. /**
  407. * Get or set basepath to `path`.
  408. *
  409. * @param {string} path
  410. * @api public
  411. */
  412. Page.prototype.base = function(path) {
  413. if (0 === arguments.length) return this._base;
  414. this._base = path;
  415. };
  416. /**
  417. * Gets the `base`, which depends on whether we are using History or
  418. * hashbang routing.
  419. * @api private
  420. */
  421. Page.prototype._getBase = function() {
  422. var base = this._base;
  423. if(!!base) return base;
  424. var loc = hasWindow && this._window && this._window.location;
  425. if(hasWindow && this._hashbang && loc && loc.protocol === 'file:') {
  426. base = loc.pathname;
  427. }
  428. return base;
  429. };
  430. /**
  431. * Get or set strict path matching to `enable`
  432. *
  433. * @param {boolean} enable
  434. * @api public
  435. */
  436. Page.prototype.strict = function(enable) {
  437. if (0 === arguments.length) return this._strict;
  438. this._strict = enable;
  439. };
  440. /**
  441. * Bind with the given `options`.
  442. *
  443. * Options:
  444. *
  445. * - `click` bind to click events [true]
  446. * - `popstate` bind to popstate [true]
  447. * - `dispatch` perform initial dispatch [true]
  448. *
  449. * @param {Object} options
  450. * @api public
  451. */
  452. Page.prototype.start = function(options) {
  453. var opts = options || {};
  454. this.configure(opts);
  455. if (false === opts.dispatch) return;
  456. this._running = true;
  457. var url;
  458. if(isLocation) {
  459. var window = this._window;
  460. var loc = window.location;
  461. if(this._hashbang && ~loc.hash.indexOf('#!')) {
  462. url = loc.hash.substr(2) + loc.search;
  463. } else if (this._hashbang) {
  464. url = loc.search + loc.hash;
  465. } else {
  466. url = loc.pathname + loc.search + loc.hash;
  467. }
  468. }
  469. this.replace(url, null, true, opts.dispatch);
  470. };
  471. /**
  472. * Unbind click and popstate event handlers.
  473. *
  474. * @api public
  475. */
  476. Page.prototype.stop = function() {
  477. if (!this._running) return;
  478. this.current = '';
  479. this.len = 0;
  480. this._running = false;
  481. var window = this._window;
  482. this._click && window.document.removeEventListener(clickEvent, this.clickHandler, false);
  483. hasWindow && window.removeEventListener('popstate', this._onpopstate, false);
  484. hasWindow && window.removeEventListener('hashchange', this._onpopstate, false);
  485. };
  486. /**
  487. * Show `path` with optional `state` object.
  488. *
  489. * @param {string} path
  490. * @param {Object=} state
  491. * @param {boolean=} dispatch
  492. * @param {boolean=} push
  493. * @return {!Context}
  494. * @api public
  495. */
  496. Page.prototype.show = function(path, state, dispatch, push) {
  497. var ctx = new Context(path, state, this),
  498. prev = this.prevContext;
  499. this.prevContext = ctx;
  500. this.current = ctx.path;
  501. if (false !== dispatch) this.dispatch(ctx, prev);
  502. if (false !== ctx.handled && false !== push) ctx.pushState();
  503. return ctx;
  504. };
  505. /**
  506. * Goes back in the history
  507. * Back should always let the current route push state and then go back.
  508. *
  509. * @param {string} path - fallback path to go back if no more history exists, if undefined defaults to page.base
  510. * @param {Object=} state
  511. * @api public
  512. */
  513. Page.prototype.back = function(path, state) {
  514. var page = this;
  515. if (this.len > 0) {
  516. var window = this._window;
  517. // this may need more testing to see if all browsers
  518. // wait for the next tick to go back in history
  519. hasHistory && window.history.back();
  520. this.len--;
  521. } else if (path) {
  522. setTimeout(function() {
  523. page.show(path, state);
  524. });
  525. } else {
  526. setTimeout(function() {
  527. page.show(page._getBase(), state);
  528. });
  529. }
  530. };
  531. /**
  532. * Register route to redirect from one path to other
  533. * or just redirect to another route
  534. *
  535. * @param {string} from - if param 'to' is undefined redirects to 'from'
  536. * @param {string=} to
  537. * @api public
  538. */
  539. Page.prototype.redirect = function(from, to) {
  540. var inst = this;
  541. // Define route from a path to another
  542. if ('string' === typeof from && 'string' === typeof to) {
  543. page.call(this, from, function(e) {
  544. setTimeout(function() {
  545. inst.replace(/** @type {!string} */ (to));
  546. }, 0);
  547. });
  548. }
  549. // Wait for the push state and replace it with another
  550. if ('string' === typeof from && 'undefined' === typeof to) {
  551. setTimeout(function() {
  552. inst.replace(from);
  553. }, 0);
  554. }
  555. };
  556. /**
  557. * Replace `path` with optional `state` object.
  558. *
  559. * @param {string} path
  560. * @param {Object=} state
  561. * @param {boolean=} init
  562. * @param {boolean=} dispatch
  563. * @return {!Context}
  564. * @api public
  565. */
  566. Page.prototype.replace = function(path, state, init, dispatch) {
  567. var ctx = new Context(path, state, this),
  568. prev = this.prevContext;
  569. this.prevContext = ctx;
  570. this.current = ctx.path;
  571. ctx.init = init;
  572. ctx.save(); // save before dispatching, which may redirect
  573. if (false !== dispatch) this.dispatch(ctx, prev);
  574. return ctx;
  575. };
  576. /**
  577. * Dispatch the given `ctx`.
  578. *
  579. * @param {Context} ctx
  580. * @api private
  581. */
  582. Page.prototype.dispatch = function(ctx, prev) {
  583. var i = 0, j = 0, page = this;
  584. function nextExit() {
  585. var fn = page.exits[j++];
  586. if (!fn) return nextEnter();
  587. fn(prev, nextExit);
  588. }
  589. function nextEnter() {
  590. var fn = page.callbacks[i++];
  591. if (ctx.path !== page.current) {
  592. ctx.handled = false;
  593. return;
  594. }
  595. if (!fn) return unhandled.call(page, ctx);
  596. fn(ctx, nextEnter);
  597. }
  598. if (prev) {
  599. nextExit();
  600. } else {
  601. nextEnter();
  602. }
  603. };
  604. /**
  605. * Register an exit route on `path` with
  606. * callback `fn()`, which will be called
  607. * on the previous context when a new
  608. * page is visited.
  609. */
  610. Page.prototype.exit = function(path, fn) {
  611. if (typeof path === 'function') {
  612. return this.exit('*', path);
  613. }
  614. var route = new Route(path, null, this);
  615. for (var i = 1; i < arguments.length; ++i) {
  616. this.exits.push(route.middleware(arguments[i]));
  617. }
  618. };
  619. /**
  620. * Handle "click" events.
  621. */
  622. /* jshint +W054 */
  623. Page.prototype.clickHandler = function(e) {
  624. if (1 !== this._which(e)) return;
  625. if (e.metaKey || e.ctrlKey || e.shiftKey) return;
  626. if (e.defaultPrevented) return;
  627. // ensure link
  628. // use shadow dom when available if not, fall back to composedPath()
  629. // for browsers that only have shady
  630. var el = e.target;
  631. var eventPath = e.path || (e.composedPath ? e.composedPath() : null);
  632. if(eventPath) {
  633. for (var i = 0; i < eventPath.length; i++) {
  634. if (!eventPath[i].nodeName) continue;
  635. if (eventPath[i].nodeName.toUpperCase() !== 'A') continue;
  636. if (!eventPath[i].href) continue;
  637. el = eventPath[i];
  638. break;
  639. }
  640. }
  641. // continue ensure link
  642. // el.nodeName for svg links are 'a' instead of 'A'
  643. while (el && 'A' !== el.nodeName.toUpperCase()) el = el.parentNode;
  644. if (!el || 'A' !== el.nodeName.toUpperCase()) return;
  645. // check if link is inside an svg
  646. // in this case, both href and target are always inside an object
  647. var svg = (typeof el.href === 'object') && el.href.constructor.name === 'SVGAnimatedString';
  648. // Ignore if tag has
  649. // 1. "download" attribute
  650. // 2. rel="external" attribute
  651. if (el.hasAttribute('download') || el.getAttribute('rel') === 'external') return;
  652. // ensure non-hash for the same path
  653. var link = el.getAttribute('href');
  654. if(!this._hashbang && this._samePath(el) && (el.hash || '#' === link)) return;
  655. // Check for mailto: in the href
  656. if (link && link.indexOf('mailto:') > -1) return;
  657. // check target
  658. // svg target is an object and its desired value is in .baseVal property
  659. if (svg ? el.target.baseVal : el.target) return;
  660. // x-origin
  661. // note: svg links that are not relative don't call click events (and skip page.js)
  662. // consequently, all svg links tested inside page.js are relative and in the same origin
  663. if (!svg && !this.sameOrigin(el.href)) return;
  664. // rebuild path
  665. // There aren't .pathname and .search properties in svg links, so we use href
  666. // Also, svg href is an object and its desired value is in .baseVal property
  667. var path = svg ? el.href.baseVal : (el.pathname + el.search + (el.hash || ''));
  668. path = path[0] !== '/' ? '/' + path : path;
  669. // strip leading "/[drive letter]:" on NW.js on Windows
  670. if (hasProcess && path.match(/^\/[a-zA-Z]:\//)) {
  671. path = path.replace(/^\/[a-zA-Z]:\//, '/');
  672. }
  673. // same page
  674. var orig = path;
  675. var pageBase = this._getBase();
  676. if (path.indexOf(pageBase) === 0) {
  677. path = path.substr(pageBase.length);
  678. }
  679. if (this._hashbang) path = path.replace('#!', '');
  680. if (pageBase && orig === path && (!isLocation || this._window.location.protocol !== 'file:')) {
  681. return;
  682. }
  683. e.preventDefault();
  684. this.show(orig);
  685. };
  686. /**
  687. * Handle "populate" events.
  688. * @api private
  689. */
  690. Page.prototype._onpopstate = (function () {
  691. var loaded = false;
  692. if ( ! hasWindow ) {
  693. return function () {};
  694. }
  695. if (hasDocument && document.readyState === 'complete') {
  696. loaded = true;
  697. } else {
  698. window.addEventListener('load', function() {
  699. setTimeout(function() {
  700. loaded = true;
  701. }, 0);
  702. });
  703. }
  704. return function onpopstate(e) {
  705. if (!loaded) return;
  706. var page = this;
  707. if (e.state) {
  708. var path = e.state.path;
  709. page.replace(path, e.state);
  710. } else if (isLocation) {
  711. var loc = page._window.location;
  712. page.show(loc.pathname + loc.search + loc.hash, undefined, undefined, false);
  713. }
  714. };
  715. })();
  716. /**
  717. * Event button.
  718. */
  719. Page.prototype._which = function(e) {
  720. e = e || (hasWindow && this._window.event);
  721. return null == e.which ? e.button : e.which;
  722. };
  723. /**
  724. * Convert to a URL object
  725. * @api private
  726. */
  727. Page.prototype._toURL = function(href) {
  728. var window = this._window;
  729. if(typeof URL === 'function' && isLocation) {
  730. return new URL(href, window.location.toString());
  731. } else if (hasDocument) {
  732. var anc = window.document.createElement('a');
  733. anc.href = href;
  734. return anc;
  735. }
  736. };
  737. /**
  738. * Check if `href` is the same origin.
  739. * @param {string} href
  740. * @api public
  741. */
  742. Page.prototype.sameOrigin = function(href) {
  743. if(!href || !isLocation) return false;
  744. var url = this._toURL(href);
  745. var window = this._window;
  746. var loc = window.location;
  747. /*
  748. When the port is the default http port 80 for http, or 443 for
  749. https, internet explorer 11 returns an empty string for loc.port,
  750. so we need to compare loc.port with an empty string if url.port
  751. is the default port 80 or 443.
  752. Also the comparition with `port` is changed from `===` to `==` because
  753. `port` can be a string sometimes. This only applies to ie11.
  754. */
  755. return loc.protocol === url.protocol &&
  756. loc.hostname === url.hostname &&
  757. (loc.port === url.port || loc.port === '' && (url.port == 80 || url.port == 443)); // jshint ignore:line
  758. };
  759. /**
  760. * @api private
  761. */
  762. Page.prototype._samePath = function(url) {
  763. if(!isLocation) return false;
  764. var window = this._window;
  765. var loc = window.location;
  766. return url.pathname === loc.pathname &&
  767. url.search === loc.search;
  768. };
  769. /**
  770. * Remove URL encoding from the given `str`.
  771. * Accommodates whitespace in both x-www-form-urlencoded
  772. * and regular percent-encoded form.
  773. *
  774. * @param {string} val - URL component to decode
  775. * @api private
  776. */
  777. Page.prototype._decodeURLEncodedURIComponent = function(val) {
  778. if (typeof val !== 'string') { return val; }
  779. return this._decodeURLComponents ? decodeURIComponent(val.replace(/\+/g, ' ')) : val;
  780. };
  781. /**
  782. * Create a new `page` instance and function
  783. */
  784. function createPage() {
  785. var pageInstance = new Page();
  786. function pageFn(/* args */) {
  787. return page.apply(pageInstance, arguments);
  788. }
  789. // Copy all of the things over. In 2.0 maybe we use setPrototypeOf
  790. pageFn.callbacks = pageInstance.callbacks;
  791. pageFn.exits = pageInstance.exits;
  792. pageFn.base = pageInstance.base.bind(pageInstance);
  793. pageFn.strict = pageInstance.strict.bind(pageInstance);
  794. pageFn.start = pageInstance.start.bind(pageInstance);
  795. pageFn.stop = pageInstance.stop.bind(pageInstance);
  796. pageFn.show = pageInstance.show.bind(pageInstance);
  797. pageFn.back = pageInstance.back.bind(pageInstance);
  798. pageFn.redirect = pageInstance.redirect.bind(pageInstance);
  799. pageFn.replace = pageInstance.replace.bind(pageInstance);
  800. pageFn.dispatch = pageInstance.dispatch.bind(pageInstance);
  801. pageFn.exit = pageInstance.exit.bind(pageInstance);
  802. pageFn.configure = pageInstance.configure.bind(pageInstance);
  803. pageFn.sameOrigin = pageInstance.sameOrigin.bind(pageInstance);
  804. pageFn.clickHandler = pageInstance.clickHandler.bind(pageInstance);
  805. pageFn.create = createPage;
  806. Object.defineProperty(pageFn, 'len', {
  807. get: function(){
  808. return pageInstance.len;
  809. },
  810. set: function(val) {
  811. pageInstance.len = val;
  812. }
  813. });
  814. Object.defineProperty(pageFn, 'current', {
  815. get: function(){
  816. return pageInstance.current;
  817. },
  818. set: function(val) {
  819. pageInstance.current = val;
  820. }
  821. });
  822. // In 2.0 these can be named exports
  823. pageFn.Context = Context;
  824. pageFn.Route = Route;
  825. return pageFn;
  826. }
  827. /**
  828. * Register `path` with callback `fn()`,
  829. * or route `path`, or redirection,
  830. * or `page.start()`.
  831. *
  832. * page(fn);
  833. * page('*', fn);
  834. * page('/user/:id', load, user);
  835. * page('/user/' + user.id, { some: 'thing' });
  836. * page('/user/' + user.id);
  837. * page('/from', '/to')
  838. * page();
  839. *
  840. * @param {string|!Function|!Object} path
  841. * @param {Function=} fn
  842. * @api public
  843. */
  844. function page(path, fn) {
  845. // <callback>
  846. if ('function' === typeof path) {
  847. return page.call(this, '*', path);
  848. }
  849. // route <path> to <callback ...>
  850. if ('function' === typeof fn) {
  851. var route = new Route(/** @type {string} */ (path), null, this);
  852. for (var i = 1; i < arguments.length; ++i) {
  853. this.callbacks.push(route.middleware(arguments[i]));
  854. }
  855. // show <path> with [state]
  856. } else if ('string' === typeof path) {
  857. this['string' === typeof fn ? 'redirect' : 'show'](path, fn);
  858. // start [options]
  859. } else {
  860. this.start(path);
  861. }
  862. }
  863. /**
  864. * Unhandled `ctx`. When it's not the initial
  865. * popstate then redirect. If you wish to handle
  866. * 404s on your own use `page('*', callback)`.
  867. *
  868. * @param {Context} ctx
  869. * @api private
  870. */
  871. function unhandled(ctx) {
  872. if (ctx.handled) return;
  873. var current;
  874. var page = this;
  875. var window = page._window;
  876. if (page._hashbang) {
  877. current = isLocation && this._getBase() + window.location.hash.replace('#!', '');
  878. } else {
  879. current = isLocation && window.location.pathname + window.location.search;
  880. }
  881. if (current === ctx.canonicalPath) return;
  882. page.stop();
  883. ctx.handled = false;
  884. isLocation && (window.location.href = ctx.canonicalPath);
  885. }
  886. /**
  887. * Escapes RegExp characters in the given string.
  888. *
  889. * @param {string} s
  890. * @api private
  891. */
  892. function escapeRegExp(s) {
  893. return s.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
  894. }
  895. /**
  896. * Initialize a new "request" `Context`
  897. * with the given `path` and optional initial `state`.
  898. *
  899. * @constructor
  900. * @param {string} path
  901. * @param {Object=} state
  902. * @api public
  903. */
  904. function Context(path, state, pageInstance) {
  905. var _page = this.page = pageInstance || page;
  906. var window = _page._window;
  907. var hashbang = _page._hashbang;
  908. var pageBase = _page._getBase();
  909. if ('/' === path[0] && 0 !== path.indexOf(pageBase)) path = pageBase + (hashbang ? '#!' : '') + path;
  910. var i = path.indexOf('?');
  911. this.canonicalPath = path;
  912. var re = new RegExp('^' + escapeRegExp(pageBase));
  913. this.path = path.replace(re, '') || '/';
  914. if (hashbang) this.path = this.path.replace('#!', '') || '/';
  915. this.title = (hasDocument && window.document.title);
  916. this.state = state || {};
  917. this.state.path = path;
  918. this.querystring = ~i ? _page._decodeURLEncodedURIComponent(path.slice(i + 1)) : '';
  919. this.pathname = _page._decodeURLEncodedURIComponent(~i ? path.slice(0, i) : path);
  920. this.params = {};
  921. // fragment
  922. this.hash = '';
  923. if (!hashbang) {
  924. if (!~this.path.indexOf('#')) return;
  925. var parts = this.path.split('#');
  926. this.path = this.pathname = parts[0];
  927. this.hash = _page._decodeURLEncodedURIComponent(parts[1]) || '';
  928. this.querystring = this.querystring.split('#')[0];
  929. }
  930. }
  931. /**
  932. * Push state.
  933. *
  934. * @api private
  935. */
  936. Context.prototype.pushState = function() {
  937. var page = this.page;
  938. var window = page._window;
  939. var hashbang = page._hashbang;
  940. page.len++;
  941. if (hasHistory) {
  942. window.history.pushState(this.state, this.title,
  943. hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
  944. }
  945. };
  946. /**
  947. * Save the context state.
  948. *
  949. * @api public
  950. */
  951. Context.prototype.save = function() {
  952. var page = this.page;
  953. if (hasHistory) {
  954. page._window.history.replaceState(this.state, this.title,
  955. page._hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
  956. }
  957. };
  958. /**
  959. * Initialize `Route` with the given HTTP `path`,
  960. * and an array of `callbacks` and `options`.
  961. *
  962. * Options:
  963. *
  964. * - `sensitive` enable case-sensitive routes
  965. * - `strict` enable strict matching for trailing slashes
  966. *
  967. * @constructor
  968. * @param {string} path
  969. * @param {Object=} options
  970. * @api private
  971. */
  972. function Route(path, options, page) {
  973. var _page = this.page = page || globalPage;
  974. var opts = options || {};
  975. opts.strict = opts.strict || _page._strict;
  976. this.path = (path === '*') ? '(.*)' : path;
  977. this.method = 'GET';
  978. this.regexp = pathToRegexp_1(this.path, this.keys = [], opts);
  979. }
  980. /**
  981. * Return route middleware with
  982. * the given callback `fn()`.
  983. *
  984. * @param {Function} fn
  985. * @return {Function}
  986. * @api public
  987. */
  988. Route.prototype.middleware = function(fn) {
  989. var self = this;
  990. return function(ctx, next) {
  991. if (self.match(ctx.path, ctx.params)) {
  992. ctx.routePath = self.path;
  993. return fn(ctx, next);
  994. }
  995. next();
  996. };
  997. };
  998. /**
  999. * Check if this route matches `path`, if so
  1000. * populate `params`.
  1001. *
  1002. * @param {string} path
  1003. * @param {Object} params
  1004. * @return {boolean}
  1005. * @api private
  1006. */
  1007. Route.prototype.match = function(path, params) {
  1008. var keys = this.keys,
  1009. qsIndex = path.indexOf('?'),
  1010. pathname = ~qsIndex ? path.slice(0, qsIndex) : path,
  1011. m = this.regexp.exec(decodeURIComponent(pathname));
  1012. if (!m) return false;
  1013. delete params[0];
  1014. for (var i = 1, len = m.length; i < len; ++i) {
  1015. var key = keys[i - 1];
  1016. var val = this.page._decodeURLEncodedURIComponent(m[i]);
  1017. if (val !== undefined || !(hasOwnProperty.call(params, key.name))) {
  1018. params[key.name] = val;
  1019. }
  1020. }
  1021. return true;
  1022. };
  1023. /**
  1024. * Module exports.
  1025. */
  1026. var globalPage = createPage();
  1027. var page_js = globalPage;
  1028. var default_1 = globalPage;
  1029. page_js.default = default_1;
  1030. export default page_js;