page.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Sanity-check the conversion and remove this comment.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS101: Remove unnecessary use of Array.from
  6. * DS102: Remove unnecessary code created because of implicit returns
  7. * DS206: Consider reworking classes to avoid initClass
  8. * DS207: Consider shorter variations of null checks
  9. * DS208: Avoid top-level this
  10. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
  11. */
  12. /*
  13. * Based on github.com/visionmedia/page.js
  14. * Licensed under the MIT license
  15. * Copyright 2012 TJ Holowaychuk <tj@vision-media.ca>
  16. */
  17. let running = false;
  18. let currentState = null;
  19. const callbacks = [];
  20. this.page = function(value, fn) {
  21. if (typeof value === 'function') {
  22. page('*', value);
  23. } else if (typeof fn === 'function') {
  24. const route = new Route(value);
  25. callbacks.push(route.middleware(fn));
  26. } else if (typeof value === 'string') {
  27. page.show(value, fn);
  28. } else {
  29. page.start(value);
  30. }
  31. };
  32. page.start = function(options) {
  33. if (options == null) { options = {}; }
  34. if (!running) {
  35. running = true;
  36. addEventListener('popstate', onpopstate);
  37. addEventListener('click', onclick);
  38. page.replace(currentPath(), null, null, true);
  39. }
  40. };
  41. page.stop = function() {
  42. if (running) {
  43. running = false;
  44. removeEventListener('click', onclick);
  45. removeEventListener('popstate', onpopstate);
  46. }
  47. };
  48. page.show = function(path, state) {
  49. let res;
  50. if (path === (currentState != null ? currentState.path : undefined)) { return; }
  51. const context = new Context(path, state);
  52. const previousState = currentState;
  53. currentState = context.state;
  54. if (res = page.dispatch(context)) {
  55. currentState = previousState;
  56. location.assign(res);
  57. } else {
  58. context.pushState();
  59. updateCanonicalLink();
  60. track();
  61. }
  62. return context;
  63. };
  64. page.replace = function(path, state, skipDispatch, init) {
  65. let result;
  66. let context = new Context(path, state || currentState);
  67. context.init = init;
  68. currentState = context.state;
  69. if (!skipDispatch) { result = page.dispatch(context); }
  70. if (result) {
  71. context = new Context(result);
  72. context.init = init;
  73. currentState = context.state;
  74. page.dispatch(context);
  75. }
  76. context.replaceState();
  77. updateCanonicalLink();
  78. if (!skipDispatch) { track(); }
  79. return context;
  80. };
  81. page.dispatch = function(context) {
  82. let i = 0;
  83. var next = function() {
  84. let fn, res;
  85. if (fn = callbacks[i++]) { res = fn(context, next); }
  86. return res;
  87. };
  88. return next();
  89. };
  90. page.canGoBack = () => !Context.isIntialState(currentState);
  91. page.canGoForward = () => !Context.isLastState(currentState);
  92. var currentPath = () => location.pathname + location.search + location.hash;
  93. class Context {
  94. static initClass() {
  95. this.initialPath = currentPath();
  96. this.sessionId = Date.now();
  97. this.stateId = 0;
  98. }
  99. static isIntialState(state) {
  100. return state.id === 0;
  101. }
  102. static isLastState(state) {
  103. return state.id === (this.stateId - 1);
  104. }
  105. static isInitialPopState(state) {
  106. return (state.path === this.initialPath) && (this.stateId === 1);
  107. }
  108. static isSameSession(state) {
  109. return state.sessionId === this.sessionId;
  110. }
  111. constructor(path, state) {
  112. if (path == null) { path = '/'; }
  113. this.path = path;
  114. if (state == null) { state = {}; }
  115. this.state = state;
  116. this.pathname = this.path.replace(/(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) => {
  117. this.query = query;
  118. this.hash = hash;
  119. return '';
  120. });
  121. if (this.state.id == null) { this.state.id = this.constructor.stateId++; }
  122. if (this.state.sessionId == null) { this.state.sessionId = this.constructor.sessionId; }
  123. this.state.path = this.path;
  124. }
  125. pushState() {
  126. history.pushState(this.state, '', this.path);
  127. }
  128. replaceState() {
  129. try { history.replaceState(this.state, '', this.path); } catch (error) {} // NS_ERROR_FAILURE in Firefox
  130. }
  131. }
  132. Context.initClass();
  133. class Route {
  134. constructor(path, options) {
  135. this.path = path;
  136. if (options == null) { options = {}; }
  137. this.keys = [];
  138. this.regexp = pathtoRegexp(this.path, this.keys);
  139. }
  140. middleware(fn) {
  141. return (context, next) => {
  142. let params;
  143. if (this.match(context.pathname, (params = []))) {
  144. context.params = params;
  145. return fn(context, next);
  146. } else {
  147. return next();
  148. }
  149. };
  150. }
  151. match(path, params) {
  152. let matchData;
  153. if (!(matchData = this.regexp.exec(path))) { return; }
  154. const iterable = matchData.slice(1);
  155. for (let i = 0; i < iterable.length; i++) {
  156. var key;
  157. var value = iterable[i];
  158. if (typeof value === 'string') { value = decodeURIComponent(value); }
  159. if ((key = this.keys[i])) {
  160. params[key.name] = value;
  161. } else {
  162. params.push(value);
  163. }
  164. }
  165. return true;
  166. }
  167. }
  168. var pathtoRegexp = function(path, keys) {
  169. if (path instanceof RegExp) { return path; }
  170. if (path instanceof Array) { path = `(${path.join('|')})`; }
  171. path = path
  172. .replace(/\/\(/g, '(?:/')
  173. .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) {
  174. if (slash == null) { slash = ''; }
  175. if (format == null) { format = ''; }
  176. keys.push({name: key, optional: !!optional});
  177. let str = optional ? '' : slash;
  178. str += '(?:';
  179. if (optional) { str += slash; }
  180. str += format;
  181. str += capture || (format ? '([^/.]+?)' : '([^/]+?)');
  182. str += ')';
  183. if (optional) { str += optional; }
  184. return str;
  185. }).replace(/([\/.])/g, '\\$1')
  186. .replace(/\*/g, '(.*)');
  187. return new RegExp(`^${path}$`);
  188. };
  189. var onpopstate = function(event) {
  190. if (!event.state || Context.isInitialPopState(event.state)) { return; }
  191. if (Context.isSameSession(event.state)) {
  192. page.replace(event.state.path, event.state);
  193. } else {
  194. location.reload();
  195. }
  196. };
  197. var onclick = function(event) {
  198. try {
  199. if ((event.which !== 1) || event.metaKey || event.ctrlKey || event.shiftKey || event.defaultPrevented) { return; }
  200. } catch (error) {
  201. return;
  202. }
  203. let link = $.eventTarget(event);
  204. while (link && (link.tagName !== 'A')) { link = link.parentNode; }
  205. if (link && !link.target && isSameOrigin(link.href)) {
  206. event.preventDefault();
  207. let path = link.pathname + link.search + link.hash;
  208. path = path.replace(/^\/\/+/, '/'); // IE11 bug
  209. page.show(path);
  210. }
  211. };
  212. var isSameOrigin = url => url.indexOf(`${location.protocol}//${location.hostname}`) === 0;
  213. var updateCanonicalLink = function() {
  214. if (!this.canonicalLink) { this.canonicalLink = document.head.querySelector('link[rel="canonical"]'); }
  215. return this.canonicalLink.setAttribute('href', `https://${location.host}${location.pathname}`);
  216. };
  217. const trackers = [];
  218. page.track = function(fn) {
  219. trackers.push(fn);
  220. };
  221. var track = function() {
  222. if (app.config.env !== 'production') { return; }
  223. if (navigator.doNotTrack === '1') { return; }
  224. if (navigator.globalPrivacyControl) { return; }
  225. const consentGiven = Cookies.get('analyticsConsent');
  226. const consentAsked = Cookies.get('analyticsConsentAsked');
  227. if (consentGiven === '1') {
  228. for (var tracker of Array.from(trackers)) { tracker.call(); }
  229. } else if ((consentGiven === undefined) && (consentAsked === undefined)) {
  230. // Only ask for consent once per browser session
  231. Cookies.set('analyticsConsentAsked', '1');
  232. new app.views.Notif('AnalyticsConsent', {autoHide: null});
  233. }
  234. };
  235. this.resetAnalytics = function() {
  236. for (var cookie of Array.from(document.cookie.split(/;\s?/))) {
  237. var name = cookie.split('=')[0];
  238. if ((name[0] === '_') && (name[1] !== '_')) {
  239. Cookies.expire(name);
  240. }
  241. }
  242. };