Source js/bauplan.router.base.js

  1. define([
  2. "lodash",
  3. "jquery",
  4. "backbone",
  5. "bauplan.authentication",
  6. "bauplan.tracker"
  7. ], function (_, jQuery, Backbone, BauplanAuthentication, Tracker) {
  8. /**
  9. * @module bauplan%router%base
  10. * @description ## RouterBase
  11. *
  12. * var BauplanRouterBase = require("bauplan.router.base");
  13. *
  14. * or as part of the Bauplan bundle
  15. *
  16. * var Bauplan = require("bauplan");
  17. * var BauplanRouterBase = Bauplan.BauplanRouterBase;
  18. *
  19. * To create and instantiate a new Router instance
  20. *
  21. * var Router = RouterBase.extend();
  22. * var router = new Router({
  23. * config: {
  24. * "foo/:param": "foo"
  25. * },
  26. * methods: {
  27. * "foo": function (param) {
  28. * …
  29. * }
  30. * }
  31. * });
  32. *
  33. * This creates a route with a URL pattern
  34. *
  35. * - foo - /^\/foo\/([^\/]+)$/ eg. /foo/lish
  36. *
  37. * When called invokes the foo method with the argument param (lish)
  38. *
  39. * The individual config properties can also be passed as objects
  40. *
  41. * var router = new Router({
  42. * config: {
  43. * "bar" : {
  44. * name: "barbar",
  45. * authenticated: true
  46. * },
  47. * "baz/:a/:b/*c": {
  48. * name: "baz",
  49. * method: function (a, b, c) {}
  50. * }
  51. * },
  52. * methods: {
  53. * "barbar": function () {}
  54. * }
  55. * });
  56. *
  57. * This creates 2 routes with the following URL patterns
  58. *
  59. * 1. barbar - /^\/bar$/ ie. /bar
  60. * 2. baz - /^\/baz\/([^\/]+)\/([^\/]+)\/(.*)$/ eg. /baz/tas/tic/al/journey
  61. *
  62. * The barbar route requires authentication and invokes the barbar method
  63. *
  64. * The baz route invokes the method defined in the method property of config.baz, with the arguments a (tas), b (tic) and c (al/journey)
  65. *
  66. *
  67. * ### Link handling
  68. *
  69. * All a element clicks are intercepted
  70. * unless:
  71. * - _target
  72. * - .external
  73. * - mailto:
  74. *
  75. * See {@link module:bauplan%router%base~window:on:click:a}
  76. *
  77. * @return {constructor} BauplanRouterBase
  78. */
  79. // alias
  80. var windoc = window.document;
  81. /**
  82. * @method jQuery%rootCookie
  83. * @description Set cookie operation at the root path
  84. * @param {string} name Cookie name
  85. * @param {string|object} value Cookie value
  86. * @param {object} [options] Options to pass to jQuery.cookie
  87. * @return Cookie value
  88. */
  89. jQuery.rootCookie = function(name, value, options) {
  90. options = options || {};
  91. options.path = "/";
  92. return jQuery.cookie(name, value, options);
  93. };
  94. /**
  95. * @method jQuery%removeRootCookie
  96. * @description Delete cookie at the root path
  97. * @param {string} name Cookie name
  98. * @param {object} [options] Options to pass to jQuery.cookie
  99. */
  100. jQuery.removeRootCookie = function(name, options) {
  101. options = options || {};
  102. options.path = "/";
  103. return jQuery.removeCookie(name, options);
  104. };
  105. jQuery.fn.outerHtml = function(){
  106. // IE, Chrome & Safari will comply with the non-standard outerHTML, all others (FF) will have a fall-back for cloning
  107. return (!this.length) ? this : (this[0].outerHTML || (
  108. function(el){
  109. var div = document.createElement("div");
  110. div.appendChild(el.cloneNode(true));
  111. var contents = div.innerHTML;
  112. div = null;
  113. return contents;
  114. })(this[0]));
  115. };
  116. // Alternatively, checkout http://madapaja.github.io/jquery.selection/
  117. jQuery.fn.getSelection = function() {
  118. var e = (this.jquery) ? this[0] : this;
  119. return (
  120. /* mozilla / dom 3.0 */
  121. ('selectionStart' in e && function() {
  122. var l = e.selectionEnd - e.selectionStart;
  123. return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) };
  124. }) ||
  125. /* exploder */
  126. (document.selection && function() {
  127. e.focus();
  128. var r = document.selection.createRange();
  129. if (r === null) {
  130. return { start: 0, end: e.value.length, length: 0 };
  131. }
  132. var re = e.createTextRange();
  133. var rc = re.duplicate();
  134. re.moveToBookmark(r.getBookmark());
  135. rc.setEndPoint('EndToStart', re);
  136. return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text };
  137. }) ||
  138. /* browser not supported */
  139. function() { return null; }
  140. )();
  141. };
  142. jQuery.fn.setSelection = function(start, end) {
  143. if (!end) {
  144. end = start;
  145. }
  146. return this.each(function() {
  147. if (this.setSelectionRange) {
  148. this.focus();
  149. this.setSelectionRange(start, end);
  150. } else if (this.createTextRange) {
  151. var range = this.createTextRange();
  152. range.collapse(true);
  153. range.moveEnd('character', end);
  154. range.moveStart('character', start);
  155. range.select();
  156. }
  157. });
  158. };
  159. /**
  160. * @method reverse
  161. * @static
  162. * @param {string} name Route name
  163. * @param {object} params Additional params for route
  164. * @description Get URL for a route
  165. * @return {string} URL for route
  166. */
  167. Backbone.Router.prototype.reverse = function (name, params) {
  168. var urlStub = this.reverseRoutes[name];
  169. var aliasMatch = urlStub.match(/^alias:(.*)/);
  170. if (aliasMatch) {
  171. urlStub = this.reverseRoutes[aliasMatch[1]];
  172. }
  173. if (urlStub === undefined) {
  174. //console.log("WTF - no match for reverse route " + name);
  175. return "";
  176. }
  177. params = params || {};
  178. for (var param in params) {
  179. var paramRegex = new RegExp("/[:\\*]"+ param);
  180. urlStub = urlStub.replace(paramRegex, "/" + params[param]);
  181. }
  182. urlStub = "/" + urlStub;
  183. // why not check value of params[param] and remove there and then?
  184. urlStub = urlStub.replace(/\/undefined.*/, "");
  185. //remove unused params
  186. urlStub = urlStub.replace(/\/[:\*].*/, "");
  187. return urlStub;
  188. };
  189. /**
  190. * @method reverseWithAuth
  191. * @static
  192. * @param {string} name Route name
  193. * @param {object} params Additional params for route
  194. * @description Get protected URL for a route
  195. * @return {string} URL for route
  196. */
  197. Backbone.Router.prototype.reverseWithAuth = function(name, params) {
  198. var routeStub = this.reverse(name, params).replace(/^\//, "");
  199. var authroute = this.getAuthRoute();
  200. return this.reverse(authroute, {route: routeStub});
  201. };
  202. var requiresLogin = {};
  203. /**
  204. * @member {array} urlList
  205. * @private
  206. * @description Simple array of URLs visited
  207. */
  208. var urlList = [];
  209. var BauplanRouterBase = Backbone.Router.extend({
  210. /**
  211. * @static
  212. * @param {object} options
  213. * @param {object} options.config Key/value pairs of route URL patterns and route options (name, method, authentication)
  214. * @description Create router instance
  215. *
  216. * - Register routes
  217. * - Register corresponding methods
  218. * - Register routes requiring authentication
  219. * - Attach event handlers to allow non-app links to work as normal
  220. * - Start Backbone.History
  221. * - Perform stored redirect if needed
  222. * - Check whether current page requires authentication
  223. * - Track pageview
  224. */
  225. initialize: function (options) {
  226. this.bauplan.Router = this;
  227. if (!options) {
  228. options = {};
  229. }
  230. var routes = [];
  231. for (var routeKey in options.config) {
  232. routes.unshift(routeKey);
  233. }
  234. for (var i = 0, routesLen = routes.length; i < routesLen; i++) {
  235. var route = routes[i];
  236. var routeVal = options.config[route];
  237. if (typeof routeVal === "string") {
  238. routeVal = {
  239. name: routeVal
  240. };
  241. }
  242. if (!routeVal.method) {
  243. routeVal.method = options.methods[routeVal.name];
  244. }
  245. this.route(route, routeVal.name, routeVal.method);
  246. // TODO: good. but let's add url matching too
  247. if (routeVal.authentication) {
  248. requiresLogin[routeVal.name] = true;
  249. }
  250. }
  251. /*this.on("all", function(route) {
  252. //alert("all got called");
  253. //console.log("all", route);
  254. this.current = route;
  255. });*/
  256. var that = this;
  257. /**
  258. * @event window:on:click:a
  259. * @description Handle all triggerings of link elements
  260. *
  261. * Allow normal link behaviour if
  262. *
  263. * - Link has a target (and not siteinfo)
  264. * - Link is an email (mailto:)
  265. * - Link has a class of "external"
  266. * - if there is an external method, use that
  267. * - otherwise, allow link to load
  268. * - Link is a fragment identifier (#)
  269. */
  270. jQuery(document).on("click", "a", function (e) {
  271. var a = e.target;
  272. if (a.tagName.toLowerCase() !== "a") {
  273. a = jQuery(a).closest("a");
  274. }
  275. if (jQuery(a).attr("target") && jQuery(a).attr("target") !== "siteinfo") {
  276. return;
  277. }
  278. var url = jQuery(a).attr("href"); //.replace(/^\//, "");
  279. if (url.match(/^mailto:/)) {
  280. return;
  281. }
  282. if (jQuery(a).hasClass("external")) {
  283. if (that.external) {
  284. that.external(url);
  285. e.preventDefault();
  286. e.stopPropagation();
  287. }
  288. return;
  289. }
  290. // || jQuery(a).hasClass("authentication")
  291. if (url && url.indexOf("#") === 0) {
  292. return;
  293. }
  294. that.navigate(url, {trigger: true}); //, replace: true,
  295. e.preventDefault();
  296. e.stopPropagation();
  297. });
  298. Backbone.history.start({ pushState: true });
  299. this.urlStack.current = windoc.location.pathname;
  300. //this.bind("all", this.allRoutes);
  301. if (this.storedRedirect()) {
  302. return;
  303. }
  304. var curl = document.location.pathname;
  305. var authdurl = this.checkAuthenticatedUrl(curl, true);
  306. if (authdurl) {
  307. return;
  308. }
  309. Tracker.pageview(curl);
  310. },
  311. /**
  312. * @static
  313. * @param {string} route Route name
  314. * @param {object} [options] Options to pass to this.navigate
  315. * @param {object} [options.trigger=true] Route triggering
  316. * @param {object} [options.options] Overriding options
  317. * @param {object} [options.params] Params to pass to route URL
  318. * @description Navigate to a route by the name of the route, rather than by URL
  319. *
  320. */
  321. callRoute: function (route, options) {
  322. options = options || {};
  323. var params = options;
  324. if (options.options) {
  325. options = _.extend({}, options.options);
  326. delete params.options;
  327. }
  328. if (params.params) {
  329. params = _.extend({}, params.params);
  330. delete options.params;
  331. }
  332. if (options.trigger === undefined) {
  333. options.trigger = true;
  334. }
  335. var url = this.reverse(route, params);
  336. this.navigate(url, options);
  337. },
  338. /**
  339. * @static
  340. * @param {string} route Route name
  341. * @param {object} [options] this.navigate options
  342. * @description Interrupt any currently invoked route and navigate to the route specified
  343. */
  344. override: function (route, options) {
  345. var r = this;
  346. setTimeout(function(){
  347. r.callRoute(route, options);
  348. }, 1);
  349. },
  350. /**
  351. * @static
  352. * @description Get a redirect route based on whether the user has registered or not
  353. * @return {string} Authentication redirect route
  354. */
  355. getAuthRoute: function () {
  356. var authroute = BauplanAuthentication.get("registered") ? "login" : "register";
  357. authroute += "-redirect";
  358. return authroute;
  359. },
  360. /**
  361. * @static
  362. * @param {string} url URL which requires authentication
  363. * @param {boolean} [replace] Whether to replace the previous entry in the user agent’s history
  364. * @description Navigate to a route where the user can authenticate before proceeding to the desired URL
  365. */
  366. callRouteWithAuth: function (url, replace) {
  367. this.callRoute(this.getAuthRoute(), {
  368. params: {
  369. route: url
  370. },
  371. options: {
  372. trigger: true,
  373. replace: replace
  374. }
  375. });
  376. },
  377. /**
  378. * @static
  379. * @param {string} url URL to look up
  380. * @description Get the route that a URL resolves to
  381. * @return {string} Route name
  382. */
  383. getRoute: function (url) {
  384. url = url.replace(/^\//, "");
  385. var BHH = Backbone.history.handlers;
  386. for (var b = 0, BHHLen = BHH.length; b < BHHLen; b++) {
  387. handler = BHH[b];
  388. if (handler.route.test(url)) {
  389. return handler.name;
  390. }
  391. }
  392. },
  393. /**
  394. * @static
  395. * @param {string} url URL to check
  396. * @param {boolean} [replace] Whether to replace the previous entry in the user agent’s history
  397. * @description Checks whether a URL needs authentication
  398. *
  399. * If it does, and the user is not authenticated, performs a redirect to an authentication page.
  400. *
  401. * Once the user has authenticated, they will be redirected to the origally requested URL
  402. */
  403. checkAuthenticatedUrl: function (url, replace) {
  404. url = url.replace(/^\//, "");
  405. var gatekeeper = requiresLogin[this.getRoute(url)];
  406. if (gatekeeper && BauplanAuthentication && !BauplanAuthentication.get("loggedIn")) {
  407. this.callRouteWithAuth(url, replace);
  408. return true;
  409. }
  410. return false;
  411. },
  412. /**
  413. * @static
  414. * @access private
  415. * @description For storing app's URL previous and current history
  416. */
  417. urlStack: {},
  418. /**
  419. * @static
  420. * @return {array} List of app URLs in order visited
  421. */
  422. getUrlList: function() {
  423. return urlList;
  424. },
  425. /**
  426. * @static
  427. * @param {string|object} name Name of previous route
  428. * @param {object} options XXXX
  429. * @param {boolean} [options.trigger=true] Whether to trigger the route
  430. * @param {boolean} [options.replace=true] Whether to replace the URL in the history
  431. * @param {boolean} [options.route] Route to use for URL
  432. * @param {boolean} [options.params] Params to pass to Route reversing
  433. * @param {boolean} [options.url] Explicit URL to use
  434. * @description Navigate to the app’s idea of the previous route
  435. */
  436. previous: function (name, options) {
  437. if (typeof name === "object") {
  438. options = name;
  439. }
  440. options = options || {
  441. trigger: true,
  442. replace: true
  443. };
  444. var url;
  445. if (name) {
  446. url = this.previousStack[name];
  447. } else {
  448. url = this.urlStack.previous;
  449. }
  450. if (!url && options.route) {
  451. url = this.reverse(options.route, options.params);
  452. }
  453. if (!url && options.url) {
  454. url = options.url;
  455. }
  456. url = url || "/";
  457. this.navigate(url, options);
  458. },
  459. /**
  460. * @static
  461. * @access private
  462. * @description Registry for storing app’s previous state
  463. */
  464. previousStack : {},
  465. /**
  466. * @static
  467. * @param {string} name Route name
  468. * @param {boolean} [ignore] Ignore this route
  469. * @description Update previous route stack
  470. */
  471. setPrevious: function(name, ignore) {
  472. this.previousStack[name] = this.urlStack.previous;
  473. },
  474. /**
  475. * @static
  476. * @description Empty method to be overriden if stored redirect functionality is desired
  477. */
  478. storedRedirect: function() {},
  479. /**
  480. * @static
  481. * @override
  482. * @param {string} url URL to invoke
  483. * @param {object} options XXXX
  484. * @description Overrides Backbone.Router.prototype.navigate
  485. *
  486. * - checks whether route is protected and if so, whether the user has the right permissions
  487. * - redirects to any stored redirect URL
  488. * - redirects to locked page if needed
  489. * - updates previous and current url stacks
  490. * - tracks pageview
  491. * - sets referrer if needed
  492. * - prevents invoking empty URLs
  493. */
  494. navigate: function(url, options) {
  495. if (BauplanAuthentication.locked()) {
  496. // better have alocked route then, eh?
  497. url = this.reverse("locked");
  498. }
  499. // console.log("navigate", url); // enable for debug?
  500. if (this.storedRedirect.apply(this, arguments)) {
  501. return;
  502. }
  503. if (!url) {
  504. //console.log("uh oh root");
  505. return;
  506. }
  507. if (!this.checkAuthenticatedUrl(url)) {
  508. this.urlStack.previous = this.urlStack.current;
  509. this.urlStack.current = url; // arguments[0]
  510. if (options.track !== false) {
  511. Tracker.pageview(url);
  512. }
  513. if (options.referrer !== false) {
  514. this.referrer = this.urlStack.previous || windoc.referrer;
  515. }
  516. Backbone.Router.prototype.navigate.apply(this, arguments);
  517. }
  518. },
  519. /**
  520. * @static
  521. * @override
  522. * @param {string} route URL pattern
  523. * @param {string} name Name of route
  524. * @param {function} callback Callback function for Backbone.Router.prototype.route
  525. * @description Register a route and update Backbone history
  526. *
  527. * Overrides Backbone.Router.prototype.route
  528. */
  529. route: function (route, name, callback) {
  530. this.reverseRoute(route, name);
  531. Backbone.Router.prototype.route.call(this, route, name, callback);
  532. // God, this is awful, but surely better than copying and pasting Backbone.Router.prototype.route
  533. var BHH = Backbone.history.handlers;
  534. BHH[0].name = name;
  535. },
  536. /**
  537. * @static
  538. * @private
  539. * @param {string} route URL pattern
  540. * @param {string} name Name of route
  541. * @description Add route URL pattern to reversed route registry
  542. */
  543. reverseRoute: function (route, name) {
  544. //this.reverseRoutes = this.reverseRoutes || {};
  545. this.reverseRoutes[name] = route;
  546. },
  547. /**
  548. * @static
  549. * @description Registry for reversed routes
  550. */
  551. reverseRoutes: {}
  552. });
  553. // Along with the awful copy and paste, maybe it's time for Bauplan.History
  554. /**
  555. * @override
  556. * @static
  557. * @param {arguments} arguments
  558. * @description Patch Backbone.History.prototype.loadUrl to enable maintaining urlList
  559. */
  560. var loadUrl = Backbone.History.prototype.loadUrl;
  561. Backbone.History.prototype.loadUrl = function () {
  562. var matched = loadUrl.apply(this, arguments);
  563. //Tracker.pageview(this.fragment);
  564. urlList.push("/" + this.fragment);
  565. return matched;
  566. };
  567. return BauplanRouterBase;
  568. });