ts/service/Router.ts - StructureJS

StructureJS

0.15.2

A class based utility library for building modular and scalable web platform applications. Features opt-in classes and utilities which provide a solid foundation and toolset to build your next project.

File: ts/service/Router.ts

  1. import StringUtil from '../util/StringUtil';
  2. import RouterEvent from '../event/RouterEvent';
  3. import Route from '../model/Route';
  4.  
  5. /**
  6. * The **Router** class is a static class allows you to add different route patterns that can be matched to help control your application. Look at the Router.{{#crossLink "Router/add:method"}}{{/crossLink}} method for more details and examples.
  7. *
  8. * @class Router
  9. * @module StructureJS
  10. * @submodule controller
  11. * @requires Route
  12. * @requires RouterEvent
  13. * @requires StringUtil
  14. * @static
  15. * @author Robert S. (www.codeBelt.com)
  16. */
  17. class Router
  18. {
  19. /**
  20. * A reference to the browser Window Object.
  21. *
  22. * @property _window
  23. * @type {Window}
  24. * @private
  25. * @static
  26. */
  27. private static _window:any = window;
  28.  
  29. /**
  30. * A list of the added Route objects.
  31. *
  32. * @property _routes
  33. * @type {Array<Route>}
  34. * @private
  35. * @static
  36. */
  37. private static _routes:Array<Route> = [];
  38.  
  39. /**
  40. * TODO: YUIDoc_comment
  41. *
  42. * @property _validators
  43. * @type {Array<Function>}
  44. * @private
  45. * @static
  46. */
  47. private static _validators:Array<Function> = [];
  48.  
  49. /**
  50. * TODO: YUIDoc_comment
  51. *
  52. * @property _validatorFunc
  53. * @type {Function}
  54. * @private
  55. * @static
  56. */
  57. private static _validatorFunc:Function = null;
  58.  
  59. /**
  60. * A reference to default route object.
  61. *
  62. * @property _defaultRoute
  63. * @type {Route}
  64. * @private
  65. * @static
  66. */
  67. private static _defaultRoute:Route = null;
  68.  
  69. /**
  70. * A reference to the hash change event that was sent from the Window Object.
  71. *
  72. * @property _hashChangeEvent
  73. * @type {any}
  74. * @private
  75. * @static
  76. */
  77. private static _hashChangeEvent:any = null;
  78.  
  79. /**
  80. * A reference to the current {{#crossLink "RouterEvent"}}{{/crossLink}} that was triggered.
  81. *
  82. * @property _currentRoute
  83. * @type {RouterEvent}
  84. * @private
  85. * @static
  86. */
  87. private static _currentRoute:RouterEvent = null;
  88.  
  89. /**
  90. * A reference to the last state object this {{#crossLink "Router"}}{{/crossLink}} creates when this
  91. * using the HTML5 History API.
  92. *
  93. * @property _lastHistoryState
  94. * @type {RouterEvent}
  95. * @private
  96. * @static
  97. */
  98. private static _lastHistoryState:{ route:string } = null;
  99.  
  100. /**
  101. * Determines if the {{#crossLink "Router"}}{{/crossLink}} should use hash or history routing.
  102. *
  103. * @property forceHashRouting
  104. * @type {boolean}
  105. * @default false
  106. * @public
  107. * @static
  108. * @example
  109. * Router.forceHashRouting = true;
  110. */
  111. public static forceHashRouting:boolean = false;
  112.  
  113. /**
  114. * Determines if the Router class is enabled or disabled.
  115. *
  116. * @property isEnabled
  117. * @type {boolean}
  118. * @readOnly
  119. * @public
  120. * @static
  121. * @example
  122. * // Read only.
  123. * console.log(Router.isEnabled);
  124. */
  125. public static isEnabled:boolean = false;
  126.  
  127. /**
  128. * The **Router.useDeepLinking** property tells the Router class weather it should change the hash url or not.
  129. * By **default** this property is set to **true**. If you set the property to **false** and using the **Router.navigateTo**
  130. * method the hash url will not change. This can be useful if you are making an application or game and you don't want the user
  131. * to know how to jump to other sections directly. See the **Router.{{#crossLink "Router/allowManualDeepLinking:property"}}{{/crossLink}}** to fully change the Router class
  132. * from relying on the hash url to an internal state service.
  133. *
  134. * @property useDeepLinking
  135. * @type {boolean}
  136. * @default true
  137. * @public
  138. * @static
  139. * @example
  140. * Router.useDeepLinking = true;
  141. */
  142. public static useDeepLinking:boolean = true;
  143.  
  144. /**
  145. * The **Router.allowManualDeepLinking** property tells the Router class weather it should check for route matches if the
  146. * hash url changes in the browser. This property only works if the **Router. {{#crossLink "Router/useDeepLinking:property"}}{{/crossLink}}** is set to **false**.
  147. * This is useful if want to use your added routes but don't want any external forces trigger your routes.
  148. *
  149. * Typically what I do for games is during development/testing I allow the hash url to change the states so testers can jump
  150. * to sections or levels easily but then when it is ready for production I set the property to **false** so users cannot jump
  151. * around if they figure out the url schema.
  152. *
  153. * @property allowManualDeepLinking
  154. * @type {boolean}
  155. * @default true
  156. * @public
  157. * @static
  158. * @example
  159. * Router.useDeepLinking = false;
  160. * Router.allowManualDeepLinking = false;
  161. */
  162. public static allowManualDeepLinking:boolean = true;
  163.  
  164. /**
  165. * The **Router.forceSlash** property tells the Router class if the **Router.{{#crossLink "Router/navigateTo:method"}}{{/crossLink}}** method is called to
  166. * make sure the hash url has a forward slash after the **#** character like this **#/**.
  167. *
  168. * @property forceSlash
  169. * @type {boolean}
  170. * @default false
  171. * @public
  172. * @static
  173. * @example
  174. * // To turn on forcing the forward slash
  175. * Router.forceSlash = true;
  176. *
  177. * // If forceSlash is set to true it will change the url from #contact/bob/ to #/contact/bob/
  178. * // when using the navigateTo method.
  179. */
  180. public static forceSlash:boolean = false;
  181.  
  182. /**
  183. * The **Router.allowMultipleMatches** property tells the Router class if it should trigger one or all routes that match a route pattern.
  184. *
  185. * @property allowMultipleMatches
  186. * @type {boolean}
  187. * @default true
  188. * @public
  189. * @static
  190. * @example
  191. * // Only allow the first route matched to be triggered.
  192. * Router.allowMultipleMatches = false;
  193. */
  194. public static allowMultipleMatches:boolean = true;
  195.  
  196. constructor()
  197. {
  198. throw new Error('[Router] Do not instantiate the Router class because it is a static class.');
  199. }
  200.  
  201. /**
  202. * The **Router.add** method allows you to listen for route patterns to be matched. When a match is found the callback will be executed passing a {{#crossLink "RouterEvent"}}{{/crossLink}}.
  203. *
  204. * @method add
  205. * @param routePattern {string} The string pattern you want to have match, which can be any of the following combinations {}, ::, *, ?, ''. See the examples below for more details.
  206. * @param callback {Function} The function that should be executed when a request matches the routePattern. It will receive a {{#crossLink "RouterEvent"}}{{/crossLink}} object.
  207. * @param callbackScope {any} The scope of the callback function that should be executed.
  208. * @public
  209. * @static
  210. * @example
  211. * // Example of adding a route listener and the function callback below.
  212. * Router.add('/games/{gameName}/:level:/', this._method, this);
  213. *
  214. * // The above route listener would match the below url:
  215. * // www.site.com/#/games/asteroids/2/
  216. *
  217. * // The Call back receives a RouterEvent object.
  218. * _onRouteHandler(routerEvent) {
  219. * console.log(routerEvent.params);
  220. * }
  221. *
  222. * Route Pattern Options:
  223. * ----------------------
  224. * **:optional:** The two colons **::** means a part of the hash url is optional for the match. The text between can be anything you want it to be.
  225. *
  226. * Router.add('/contact/:name:/', this._method, this);
  227. *
  228. * // Will match one of the following:
  229. * // www.site.com/#/contact/
  230. * // www.site.com/#/contact/heather/
  231. * // www.site.com/#/contact/john/
  232. *
  233. *
  234. * **{required}** The two curly brackets **{}** means a part of the hash url is required for the match. The text between can be anything you want it to be.
  235. *
  236. * Router.add('/product/{productName}/', this._method, this);
  237. *
  238. * // Will match one of the following:
  239. * // www.site.com/#/product/shoes/
  240. * // www.site.com/#/product/jackets/
  241. *
  242. *
  243. * **\*** The asterisk character means it will match all or part of part the hash url.
  244. *
  245. * Router.add('*', this._method, this);
  246. *
  247. * // Will match one of the following:
  248. * // www.site.com/#/anything/
  249. * // www.site.com/#/matches/any/hash/url/
  250. * // www.site.com/#/really/it/matches/any/and/all/hash/urls/
  251. *
  252. *
  253. * **?** The question mark character means it will match a query string for the hash url.
  254. *
  255. * Router.add('?', this._method, this);
  256. *
  257. * // Will match one of the following:
  258. * // www.site.com/#/?one=1&two=2&three=3
  259. * // www.site.com/#?one=1&two=2&three=3
  260. *
  261. *
  262. * **''** The empty string means it will match when there are no hash url.
  263. *
  264. * Router.add('', this._method, this);
  265. * Router.add('/', this._method, this);
  266. *
  267. * // Will match one of the following:
  268. * // www.site.com/
  269. * // www.site.com/#/
  270. *
  271. *
  272. * Other possible combinations but not limited too:
  273. *
  274. * Router.add('/games/{gameName}/:level:/', this._method1, this);
  275. * Router.add('/{category}/blog/', this._method2, this);
  276. * Router.add('/home/?', this._method3, this);
  277. * Router.add('/about/*', this._method4, this);
  278. *
  279. */
  280. public static add(routePattern:string, callback:Function, callbackScope:any):void
  281. {
  282. Router.enable();
  283.  
  284. const route:Route = new Route(routePattern, callback, callbackScope);
  285.  
  286. Router._routes.push(route);
  287. }
  288.  
  289. /**
  290. * The **Router.remove** method will remove one of the added routes.
  291. *
  292. * @method remove
  293. * @param routePattern {string} Must be the same string pattern you pasted into the {{#crossLink "Router/add:method"}}{{/crossLink}} method.
  294. * @param callback {Function} Must be the same function you pasted into the {{#crossLink "Router/add:method"}}{{/crossLink}} method.
  295. * @param callbackScope {any} Must be the same scope off the callback pattern you pasted into the {{#crossLink "Router/add:method"}}{{/crossLink}} method.
  296. * @public
  297. * @static
  298. * @example
  299. * // Example of adding a route listener.
  300. * Router.add('/games/{gameName}/:level:/', this._method, this);
  301. *
  302. * // Example of removing the same added route listener above.
  303. * Router.remove('/games/{gameName}/:level:/', this._method, this);
  304. */
  305. public static remove(routePattern:string, callback:Function, callbackScope:any):void
  306. {
  307. let route:Route;
  308. // Since we are removing (splice) from routes we need to check the length every iteration.
  309. for (let i = Router._routes.length - 1; i >= 0; i--)
  310. {
  311. route = Router._routes[i];
  312. if (route.routePattern === routePattern && route.callback === callback && route.callbackScope === callbackScope)
  313. {
  314. Router._routes.splice(i, 1);
  315. }
  316. }
  317. }
  318.  
  319. /**
  320. * The **Router.addDefault** method is meant to trigger a callback function if there are no route matches are found.
  321. *
  322. * @method addDefault
  323. * @param callback {Function}
  324. * @param callbackScope {any}
  325. * @public
  326. * @static
  327. * @example
  328. * Router.addDefault(this._noRoutesFoundHandler, this);
  329. */
  330. public static addDefault(callback:Function, callbackScope:any):void
  331. {
  332. Router._defaultRoute = new Route('', callback, callbackScope);
  333. }
  334.  
  335. /**
  336. * The **Router.removeDefault** method will remove the default callback that was added by the **Router.addDefault** method.
  337. *
  338. * @method removeDefault
  339. * @public
  340. * @static
  341. * @example
  342. * Router.removeDefault();
  343. */
  344. public static removeDefault():void
  345. {
  346. Router._defaultRoute = null;
  347. }
  348.  
  349. /**
  350. * Gets the current hash url minus the # or #! symbol(s).
  351. *
  352. * @method getHash
  353. * @public
  354. * @static
  355. * @return {string} Returns current hash url minus the # or #! symbol(s).
  356. * @example
  357. * let str = Router.getHash();
  358. */
  359. public static getHash():string
  360. {
  361. const hash:string = Router._window.location.hash;
  362. const strIndex:number = (hash.substr(0, 2) === '#!') ? 2 : 1;
  363.  
  364. return hash.substring(strIndex); // Return everything after # or #!
  365. }
  366.  
  367. /**
  368. * The **Router.enable** method will allow the Router class to listen for the hashchange event. By default the Router class is enabled.
  369. *
  370. * @method enable
  371. * @public
  372. * @static
  373. * @example
  374. * Router.enable();
  375. */
  376. public static enable():void
  377. {
  378. if (Router.isEnabled === true)
  379. {
  380. return;
  381. }
  382.  
  383. if (Router._window.addEventListener)
  384. {
  385. Router._window.addEventListener('hashchange', Router._onHashChange, false);
  386. Router._window.addEventListener('popstate', Router._onHistoryChange, false);
  387. }
  388. else
  389. {
  390. Router._window.attachEvent('onhashchange', Router._onHashChange);
  391. Router._window.attachEvent('onpopstate', Router._onHistoryChange);
  392. }
  393.  
  394. Router.isEnabled = true;
  395. }
  396.  
  397. /**
  398. * The **Router.disable** method will stop the Router class from listening for the hashchange event.
  399. *
  400. * @method disable
  401. * @public
  402. * @static
  403. * @example
  404. * Router.disable();
  405. */
  406. public static disable():void
  407. {
  408. if (Router.isEnabled === false)
  409. {
  410. return;
  411. }
  412.  
  413. if (Router._window.removeEventListener)
  414. {
  415. Router._window.removeEventListener('hashchange', Router._onHashChange);
  416. Router._window.removeEventListener('popstate', Router._onHistoryChange);
  417. }
  418. else
  419. {
  420. Router._window.detachEvent('onhashchange', Router._onHashChange);
  421. Router._window.detachEvent('onpopstate', Router._onHistoryChange);
  422. }
  423.  
  424. Router.isEnabled = false;
  425. }
  426.  
  427. /**
  428. * The **Router.start** method is meant to trigger or check the hash url on page load.
  429. * Either you can call this method after you add all your routers or after all data is loaded.
  430. * It is recommend you only call this once per page or application instantiation.
  431. *
  432. * @method start
  433. * @public
  434. * @static
  435. * @example
  436. * // Example of adding routes and calling the start method.
  437. * Router.add('/games/{gameName}/:level:/', this._method1, this);
  438. * Router.add('/{category}/blog/', this._method2, this);
  439. *
  440. * Router.start();
  441. */
  442. public static start():void
  443. {
  444. Router.forceHashRouting = (window.history && window.history.pushState) ? Router.forceHashRouting : true;
  445.  
  446. if (Router.forceHashRouting === true) {
  447. setTimeout(Router._onHashChange, 1);
  448. } else {
  449. setTimeout(Router._onHistoryChange, 1);
  450. }
  451. }
  452.  
  453. /**
  454. * The **Router.navigateTo** method allows you to change the hash url and to trigger a route
  455. * that matches the string value. The second parameter is **silent** and is **false** by
  456. * default. This allows you to update the hash url without causing a route callback to be
  457. * executed.
  458. *
  459. * @method navigateTo
  460. * @param route {String}
  461. * @param [silent=false] {Boolean}
  462. * @param [disableHistory=false] {Boolean}
  463. * @public
  464. * @static
  465. * @example
  466. * // This will update the hash url and trigger the matching route.
  467. * Router.navigateTo('/games/asteroids/2/');
  468. *
  469. * // This will update the hash url but will not trigger the matching route.
  470. * Router.navigateTo('/games/asteroids/2/', true);
  471. *
  472. * // This will not update the hash url but will trigger the matching route.
  473. * Router.navigateTo('/games/asteroids/2/', true, true);
  474. */
  475. public static navigateTo(route, silent:boolean = false, disableHistory:boolean = false):void
  476. {
  477. if (Router.isEnabled === false)
  478. {
  479. return;
  480. }
  481.  
  482. if (route.charAt(0) === '#')
  483. {
  484. const strIndex = (route.substr(0, 2) === '#!') ? 2 : 1;
  485. route = route.substring(strIndex);
  486. }
  487.  
  488. if (Router.forceHashRouting === true)
  489. {
  490. // Enforce starting slash
  491. if (route.charAt(0) !== '/' && Router.forceSlash === true)
  492. {
  493. route = '/' + route;
  494. }
  495.  
  496. if (disableHistory === true)
  497. {
  498. Router._changeRoute(route);
  499. return;
  500. }
  501.  
  502. if (Router.useDeepLinking === true)
  503. {
  504. if (silent === true)
  505. {
  506. Router.disable();
  507. setTimeout(function ()
  508. {
  509. window.location.hash = route;
  510. setTimeout(Router.enable, 1);
  511. }, 1);
  512. }
  513. else
  514. {
  515. setTimeout(function ()
  516. {
  517. window.location.hash = route;
  518. }, 1);
  519. }
  520. }
  521. else
  522. {
  523. Router._changeRoute(route);
  524. }
  525. }
  526. else
  527. {
  528. Router._lastHistoryState = window.history.state;
  529.  
  530. if (Router.useDeepLinking === true)
  531. {
  532. window.history.pushState({ route: route }, null, route);
  533. }
  534.  
  535. Router._changeRoute(route);
  536. }
  537. }
  538.  
  539. /**
  540. * The **Router.clear** will remove all route's and the default route from the Router class.
  541. *
  542. * @method clear
  543. * @public
  544. * @static
  545. * @example
  546. * Router.clear();
  547. */
  548. public clear():void
  549. {
  550. Router._routes = [];
  551. Router._defaultRoute = null;
  552. Router._hashChangeEvent = null;
  553. }
  554.  
  555. /**
  556. * The **Router.destroy** method will null out all references to other objects in the Router class.
  557. *
  558. * @method destroy
  559. * @public
  560. * @static
  561. * @example
  562. * Router.destroy();
  563. */
  564. public destroy():void
  565. {
  566. Router._window = null;
  567. Router._routes = null;
  568. Router._defaultRoute = null;
  569. Router._hashChangeEvent = null;
  570. }
  571.  
  572. /**
  573. * A simple helper method to create a url route from an unlimited number of arguments.
  574. *
  575. * @method buildRoute
  576. * @param ...rest {...rest}
  577. * @return {string}
  578. * @public
  579. * @static
  580. * @example
  581. * const someProperty = 'api/endpoint';
  582. * const queryObject = {type: 'car', name: encodeURIComponent('Telsa Motors')};
  583. *
  584. * Router.buildRoute(someProperty, 'path', 7, queryObject);
  585. *
  586. * //Creates 'api/endpoint/path/7?type=car&name=Telsa%20Motors'
  587. */
  588. public static buildRoute(...rest):string
  589. {
  590. let clone = rest.slice(0);
  591.  
  592. clone.forEach((value, index) => {
  593. if (typeof value === 'object')
  594. {
  595. clone[index] = `?${StringUtil.toQueryString(value)}`;
  596. }
  597. });
  598.  
  599. // Remove any empty strings from the array
  600. clone = clone.filter(Boolean);
  601.  
  602. let route = clone.join('/');
  603. // Remove extra back slashes
  604. route = route.replace(/\/\//g, '/');
  605. // Add back slash since we remove it from the "http://"
  606. route = route.replace(':/', '://');
  607. // Remove the back slash in front of a question mark
  608. route = route.replace('/?', '?');
  609.  
  610. return route
  611. }
  612.  
  613. /**
  614. * Returns the current router event that was last triggered.
  615. *
  616. * @method getCurrentRoute
  617. * @public
  618. * @static
  619. * @example
  620. * Router.getCurrentRoute();
  621. */
  622. public static getCurrentRoute():RouterEvent
  623. {
  624. return this._currentRoute;
  625. }
  626.  
  627. /**
  628. * TODO: YUIDoc_comment
  629. *
  630. * @method validate
  631. * @param func {Function} The function you wanted called if the validation failed.
  632. * @public
  633. * @static
  634. * @example
  635. * Router.validate((routerEvent, next) => {
  636. * const allowRouteChange = this._someMethodCheck();
  637. *
  638. * if (allowRouteChange == false) {
  639. * next(() => {
  640. * // Do something here.
  641. * // For example you can call Router.navigateTo to change the route.
  642. * });
  643. * } else {
  644. * next();
  645. * }
  646. * });
  647. */
  648. public static validate(func:Function):void
  649. {
  650. Router._validators.push(func);
  651. }
  652.  
  653. /**
  654. * This method will be called if the Window object dispatches a HashChangeEvent.
  655. * This method will not be called if the Router is disabled.
  656. *
  657. * @method _onHashChange
  658. * @param event {HashChangeEvent}
  659. * @private
  660. * @static
  661. */
  662. private static _onHashChange(event):void
  663. {
  664. if (Router.allowManualDeepLinking !== false && Router.useDeepLinking !== false)
  665. {
  666. Router._hashChangeEvent = event;
  667. var hash = Router.getHash();
  668. Router._changeRoute(hash);
  669. }
  670. else
  671. {
  672. Router._changeRoute('');
  673. }
  674. }
  675.  
  676. /**
  677. * This method will be called if the Window object dispatches a popstate event.
  678. * This method will not be called if the Router is disabled.
  679. *
  680. * @method _onHistoryChange
  681. * @param event {HashChangeEvent}
  682. * @private
  683. * @static
  684. */
  685. private static _onHistoryChange(event)
  686. {
  687. if (Router.forceHashRouting === true)
  688. {
  689. return;
  690. }
  691.  
  692. if (Router.allowManualDeepLinking !== false && Router.useDeepLinking !== false)
  693. {
  694. if (event != null)
  695. {
  696. const state:any = event.state;
  697. Router._changeRoute(state.route);
  698. }
  699. else
  700. {
  701. const route = location.pathname + location.search + location.hash;
  702.  
  703. if (Router.useDeepLinking === true)
  704. {
  705. window.history.replaceState({ route: route }, null, null);
  706. }
  707.  
  708. Router._changeRoute(route);
  709. }
  710. }
  711. else
  712. {
  713. Router._changeRoute('');
  714. }
  715. }
  716.  
  717. /**
  718. * The method is responsible for check if one of the routes matches the string value passed in.
  719. *
  720. * @method _changeRoute
  721. * @param hash {string}
  722. * @private
  723. * @static
  724. */
  725. private static _changeRoute(hash:string):void
  726. {
  727. let route:Route;
  728. let match:any;
  729. let routerEvent:RouterEvent = null;
  730.  
  731. // Loop through all the route's. Note: we need to check the length every loop in case one was removed.
  732. for (let i = 0; i < Router._routes.length; i++)
  733. {
  734. route = Router._routes[i];
  735. match = route.match(hash);
  736.  
  737. // If there is a match.
  738. if (match !== null)
  739. {
  740. routerEvent = new RouterEvent();
  741. routerEvent.route = match.shift();
  742. routerEvent.params = match.slice(0, match.length);
  743. routerEvent.routePattern = route.routePattern;
  744. routerEvent.query = (hash.indexOf('?') > -1) ? StringUtil.queryStringToObject(hash) : null;
  745. routerEvent.target = Router;
  746. routerEvent.currentTarget = Router;
  747.  
  748. // Remove any empty strings in the array due to the :optional: route pattern.
  749. // Since we are removing (splice) from params we need to check the length every iteration.
  750. for (let j = routerEvent.params.length - 1; j >= 0; j--)
  751. {
  752. if (routerEvent.params[j] === '')
  753. {
  754. routerEvent.params.splice(j, 1);
  755. }
  756. }
  757.  
  758. // If there was a hash change event then set the info we want to send.
  759. if (Router._hashChangeEvent != null)
  760. {
  761. routerEvent.newURL = Router._hashChangeEvent.newURL;
  762. routerEvent.oldURL = Router._hashChangeEvent.oldURL;
  763. }
  764. else if (window.history && window.history.state)
  765. {
  766. routerEvent.newURL = hash;
  767. routerEvent.oldURL = (Router._lastHistoryState === null) ? null : Router._lastHistoryState.route;
  768.  
  769. Router._lastHistoryState = { route: routerEvent.newURL }
  770. }
  771. else
  772. {
  773. routerEvent.newURL = window.location.href;
  774. }
  775.  
  776. const allowRouteChange:boolean = Router._allowRouteChange(routerEvent);
  777.  
  778. if (allowRouteChange === true)
  779. {
  780. Router._currentRoute = routerEvent;
  781.  
  782. // Execute the callback function and pass the route event.
  783. route.callback.call(route.callbackScope, routerEvent);
  784.  
  785. // Only trigger the first route and stop checking.
  786. if (Router.allowMultipleMatches === false)
  787. {
  788. break;
  789. }
  790. }
  791. else
  792. {
  793. break;
  794. }
  795. }
  796. }
  797.  
  798. // If there are no route's matched and there is a default route. Then call that default route.
  799. if (routerEvent === null)
  800. {
  801. routerEvent = new RouterEvent();
  802. routerEvent.route = hash;
  803. routerEvent.query = (hash.indexOf('?') > -1) ? StringUtil.queryStringToObject(hash) : null;
  804. routerEvent.target = Router;
  805. routerEvent.currentTarget = Router;
  806.  
  807. if (Router._hashChangeEvent != null)
  808. {
  809. routerEvent.newURL = Router._hashChangeEvent.newURL;
  810. routerEvent.oldURL = Router._hashChangeEvent.oldURL;
  811. }
  812. else
  813. {
  814. routerEvent.newURL = window.location.href;
  815. }
  816.  
  817. const allowRouteChange:boolean = Router._allowRouteChange(routerEvent);
  818.  
  819. if (allowRouteChange === true)
  820. {
  821. Router._currentRoute = routerEvent;
  822.  
  823. if (Router._defaultRoute !== null)
  824. {
  825. Router._defaultRoute.callback.call(Router._defaultRoute.callbackScope, routerEvent);
  826. }
  827. }
  828. }
  829.  
  830. Router._hashChangeEvent = null;
  831. if (Router._validatorFunc != null)
  832. {
  833. Router._validatorFunc();
  834. }
  835. }
  836.  
  837. /**
  838. * TODO: YUIDoc_comment
  839. *
  840. * @method _allowRouteChange
  841. * @private
  842. * @static
  843. */
  844. private static _allowRouteChange(routerEvent:RouterEvent):boolean
  845. {
  846. Router._validatorFunc = null;
  847.  
  848. for (let i:number = 0; i < Router._validators.length; i++)
  849. {
  850. const func:Function = Router._validators[i];
  851.  
  852. if (Router._validatorFunc != null)
  853. {
  854. break;
  855. }
  856.  
  857. const callback:Function = (back:Function = null) =>
  858. {
  859. Router._validatorFunc = back;
  860. };
  861.  
  862. func(routerEvent, callback);
  863. }
  864.  
  865. return Router._validatorFunc == null;
  866. }
  867.  
  868. }
  869.  
  870. export default Router;
  871.