page.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. /*
  2. * Based on github.com/visionmedia/page.js
  3. * Licensed under the MIT license
  4. * Copyright 2012 TJ Holowaychuk <tj@vision-media.ca>
  5. */
  6. let running = false;
  7. let currentState = null;
  8. const callbacks = [];
  9. this.page = function (value, fn) {
  10. if (typeof value === "function") {
  11. page("*", value);
  12. } else if (typeof fn === "function") {
  13. const route = new Route(value);
  14. callbacks.push(route.middleware(fn));
  15. } else if (typeof value === "string") {
  16. page.show(value, fn);
  17. } else {
  18. page.start(value);
  19. }
  20. };
  21. page.start = function (options) {
  22. if (options == null) {
  23. options = {};
  24. }
  25. if (!running) {
  26. running = true;
  27. addEventListener("popstate", onpopstate);
  28. addEventListener("click", onclick);
  29. page.replace(currentPath(), null, null, true);
  30. }
  31. };
  32. page.stop = function () {
  33. if (running) {
  34. running = false;
  35. removeEventListener("click", onclick);
  36. removeEventListener("popstate", onpopstate);
  37. }
  38. };
  39. page.show = function (path, state) {
  40. if (path === currentState?.path) {
  41. return;
  42. }
  43. const context = new Context(path, state);
  44. const previousState = currentState;
  45. currentState = context.state;
  46. const res = page.dispatch(context);
  47. if (res) {
  48. currentState = previousState;
  49. location.assign(res);
  50. } else {
  51. context.pushState();
  52. updateCanonicalLink();
  53. track();
  54. }
  55. return context;
  56. };
  57. page.replace = function (path, state, skipDispatch, init) {
  58. let result;
  59. let context = new Context(path, state || currentState);
  60. context.init = init;
  61. currentState = context.state;
  62. if (!skipDispatch) {
  63. result = page.dispatch(context);
  64. }
  65. if (result) {
  66. context = new Context(result);
  67. context.init = init;
  68. currentState = context.state;
  69. page.dispatch(context);
  70. }
  71. context.replaceState();
  72. updateCanonicalLink();
  73. if (!skipDispatch) {
  74. track();
  75. }
  76. return context;
  77. };
  78. page.dispatch = function (context) {
  79. let i = 0;
  80. const next = function () {
  81. let fn = callbacks[i++];
  82. return fn?.(context, next);
  83. };
  84. return next();
  85. };
  86. page.canGoBack = () => !Context.isIntialState(currentState);
  87. page.canGoForward = () => !Context.isLastState(currentState);
  88. const currentPath = () => location.pathname + location.search + location.hash;
  89. class Context {
  90. static isIntialState(state) {
  91. return state.id === 0;
  92. }
  93. static isLastState(state) {
  94. return state.id === this.stateId - 1;
  95. }
  96. static isInitialPopState(state) {
  97. return state.path === this.initialPath && this.stateId === 1;
  98. }
  99. static isSameSession(state) {
  100. return state.sessionId === this.sessionId;
  101. }
  102. constructor(path, state) {
  103. this.initialPath = currentPath();
  104. this.sessionId = Date.now();
  105. this.stateId = 0;
  106. if (path == null) {
  107. path = "/";
  108. }
  109. this.path = path;
  110. if (state == null) {
  111. state = {};
  112. }
  113. this.state = state;
  114. this.pathname = this.path.replace(
  115. /(?:\?([^#]*))?(?:#(.*))?$/,
  116. (_, query, hash) => {
  117. this.query = query;
  118. this.hash = hash;
  119. return "";
  120. },
  121. );
  122. if (this.state.id == null) {
  123. this.state.id = this.constructor.stateId++;
  124. }
  125. if (this.state.sessionId == null) {
  126. this.state.sessionId = this.constructor.sessionId;
  127. }
  128. this.state.path = this.path;
  129. }
  130. pushState() {
  131. history.pushState(this.state, "", this.path);
  132. }
  133. replaceState() {
  134. try {
  135. history.replaceState(this.state, "", this.path);
  136. } catch (error) {} // NS_ERROR_FAILURE in Firefox
  137. }
  138. }
  139. class Route {
  140. constructor(path, options) {
  141. this.path = path;
  142. if (options == null) {
  143. options = {};
  144. }
  145. this.keys = [];
  146. this.regexp = pathToRegexp(this.path, this.keys);
  147. }
  148. middleware(fn) {
  149. return (context, next) => {
  150. let params = [];
  151. if (this.match(context.pathname, params)) {
  152. context.params = params;
  153. return fn(context, next);
  154. } else {
  155. return next();
  156. }
  157. };
  158. }
  159. match(path, params) {
  160. const matchData = this.regexp.exec(path);
  161. if (!matchData) {
  162. return;
  163. }
  164. const iterable = matchData.slice(1);
  165. for (let i = 0; i < iterable.length; i++) {
  166. var key = this.keys[i];
  167. var value = iterable[i];
  168. if (typeof value === "string") {
  169. value = decodeURIComponent(value);
  170. }
  171. if (key) {
  172. params[key.name] = value;
  173. } else {
  174. params.push(value);
  175. }
  176. }
  177. return true;
  178. }
  179. }
  180. var pathToRegexp = function (path, keys) {
  181. if (path instanceof RegExp) {
  182. return path;
  183. }
  184. if (path instanceof Array) {
  185. path = `(${path.join("|")})`;
  186. }
  187. path = path
  188. .replace(/\/\(/g, "(?:/")
  189. .replace(
  190. /(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g,
  191. (_, slash, format, key, capture, optional) => {
  192. if (slash == null) {
  193. slash = "";
  194. }
  195. if (format == null) {
  196. format = "";
  197. }
  198. keys.push({ name: key, optional: !!optional });
  199. let str = optional ? "" : slash;
  200. str += "(?:";
  201. if (optional) {
  202. str += slash;
  203. }
  204. str += format;
  205. str += capture || (format ? "([^/.]+?)" : "([^/]+?)");
  206. str += ")";
  207. if (optional) {
  208. str += optional;
  209. }
  210. return str;
  211. },
  212. )
  213. .replace(/([\/.])/g, "\\$1")
  214. .replace(/\*/g, "(.*)");
  215. return new RegExp(`^${path}$`);
  216. };
  217. var onpopstate = function (event) {
  218. if (!event.state || Context.isInitialPopState(event.state)) {
  219. return;
  220. }
  221. if (Context.isSameSession(event.state)) {
  222. page.replace(event.state.path, event.state);
  223. } else {
  224. location.reload();
  225. }
  226. };
  227. var onclick = function (event) {
  228. try {
  229. if (
  230. event.which !== 1 ||
  231. event.metaKey ||
  232. event.ctrlKey ||
  233. event.shiftKey ||
  234. event.defaultPrevented
  235. ) {
  236. return;
  237. }
  238. } catch (error) {
  239. return;
  240. }
  241. let link = $.eventTarget(event);
  242. while (link && !(link.tagName === "A" || link.tagName === "a")) {
  243. link = link.parentNode;
  244. }
  245. if (!link) return;
  246. // If the `<a>` is in an SVG, its attributes are `SVGAnimatedString`s
  247. // instead of strings
  248. let href = link.href instanceof SVGAnimatedString
  249. ? new URL(link.href.baseVal, location.href).href
  250. : link.href;
  251. let target = link.target instanceof SVGAnimatedString
  252. ? link.target.baseVal
  253. : link.target;
  254. if (!target && isSameOrigin(href)) {
  255. event.preventDefault();
  256. let parsedHref = new URL(href);
  257. let path = parsedHref.pathname + parsedHref.search + parsedHref.hash;
  258. path = path.replace(/^\/\/+/, "/"); // IE11 bug
  259. page.show(path);
  260. }
  261. };
  262. var isSameOrigin = (url) =>
  263. url.startsWith(`${location.protocol}//${location.hostname}`);
  264. var updateCanonicalLink = function () {
  265. if (!this.canonicalLink) {
  266. this.canonicalLink = document.head.querySelector('link[rel="canonical"]');
  267. }
  268. return this.canonicalLink.setAttribute(
  269. "href",
  270. `https://${location.host}${location.pathname}`,
  271. );
  272. };
  273. const trackers = [];
  274. page.track = function (fn) {
  275. trackers.push(fn);
  276. };
  277. var track = function () {
  278. if (app.config.env !== "production") {
  279. return;
  280. }
  281. if (navigator.doNotTrack === "1") {
  282. return;
  283. }
  284. if (navigator.globalPrivacyControl) {
  285. return;
  286. }
  287. const consentGiven = Cookies.get("analyticsConsent");
  288. const consentAsked = Cookies.get("analyticsConsentAsked");
  289. if (consentGiven === "1") {
  290. for (var tracker of trackers) {
  291. tracker.call();
  292. }
  293. } else if (consentGiven === undefined && consentAsked === undefined) {
  294. // Only ask for consent once per browser session
  295. Cookies.set("analyticsConsentAsked", "1");
  296. new app.views.Notif("AnalyticsConsent", { autoHide: null });
  297. }
  298. };
  299. this.resetAnalytics = function () {
  300. for (var cookie of document.cookie.split(/;\s?/)) {
  301. var name = cookie.split("=")[0];
  302. if (name[0] === "_" && name[1] !== "_") {
  303. Cookies.expire(name);
  304. }
  305. }
  306. };