app.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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. * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
  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. this.app = {
  13. _$: $,
  14. _$$: $$,
  15. _page: page,
  16. collections: {},
  17. models: {},
  18. templates: {},
  19. views: {},
  20. init() {
  21. try {
  22. this.initErrorTracking();
  23. } catch (error) {}
  24. if (!this.browserCheck()) {
  25. return;
  26. }
  27. this.el = $("._app");
  28. this.localStorage = new LocalStorageStore();
  29. if (app.ServiceWorker.isEnabled()) {
  30. this.serviceWorker = new app.ServiceWorker();
  31. }
  32. this.settings = new app.Settings();
  33. this.db = new app.DB();
  34. this.settings.initLayout();
  35. this.docs = new app.collections.Docs();
  36. this.disabledDocs = new app.collections.Docs();
  37. this.entries = new app.collections.Entries();
  38. this.router = new app.Router();
  39. this.shortcuts = new app.Shortcuts();
  40. this.document = new app.views.Document();
  41. if (this.isMobile()) {
  42. this.mobile = new app.views.Mobile();
  43. }
  44. if (document.body.hasAttribute("data-doc")) {
  45. this.DOC = JSON.parse(document.body.getAttribute("data-doc"));
  46. this.bootOne();
  47. } else if (this.DOCS) {
  48. this.bootAll();
  49. } else {
  50. this.onBootError();
  51. }
  52. },
  53. browserCheck() {
  54. if (this.isSupportedBrowser()) {
  55. return true;
  56. }
  57. document.body.innerHTML = app.templates.unsupportedBrowser;
  58. this.hideLoadingScreen();
  59. return false;
  60. },
  61. initErrorTracking() {
  62. // Show a warning message and don't track errors when the app is loaded
  63. // from a domain other than our own, because things are likely to break.
  64. // (e.g. cross-domain requests)
  65. if (this.isInvalidLocation()) {
  66. new app.views.Notif("InvalidLocation");
  67. } else {
  68. if (this.config.sentry_dsn) {
  69. Raven.config(this.config.sentry_dsn, {
  70. release: this.config.release,
  71. whitelistUrls: [/devdocs/],
  72. includePaths: [/devdocs/],
  73. ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/],
  74. tags: {
  75. mode: this.isSingleDoc() ? "single" : "full",
  76. iframe: (window.top !== window).toString(),
  77. electron: (!!__guard__(
  78. window.process != null ? window.process.versions : undefined,
  79. (x) => x.electron,
  80. )).toString(),
  81. },
  82. shouldSendCallback: () => {
  83. try {
  84. if (this.isInjectionError()) {
  85. this.onInjectionError();
  86. return false;
  87. }
  88. if (this.isAndroidWebview()) {
  89. return false;
  90. }
  91. } catch (error) {}
  92. return true;
  93. },
  94. dataCallback(data) {
  95. try {
  96. $.extend(data.user || (data.user = {}), app.settings.dump());
  97. if (data.user.docs) {
  98. data.user.docs = data.user.docs.split("/");
  99. }
  100. if (app.lastIDBTransaction) {
  101. data.user.lastIDBTransaction = app.lastIDBTransaction;
  102. }
  103. data.tags.scriptCount = document.scripts.length;
  104. } catch (error) {}
  105. return data;
  106. },
  107. }).install();
  108. }
  109. this.previousErrorHandler = onerror;
  110. window.onerror = this.onWindowError.bind(this);
  111. CookiesStore.onBlocked = this.onCookieBlocked;
  112. }
  113. },
  114. bootOne() {
  115. this.doc = new app.models.Doc(this.DOC);
  116. this.docs.reset([this.doc]);
  117. this.doc.load(this.start.bind(this), this.onBootError.bind(this), {
  118. readCache: true,
  119. });
  120. new app.views.Notice("singleDoc", this.doc);
  121. delete this.DOC;
  122. },
  123. bootAll() {
  124. const docs = this.settings.getDocs();
  125. for (var doc of Array.from(this.DOCS)) {
  126. (docs.indexOf(doc.slug) >= 0 ? this.docs : this.disabledDocs).add(doc);
  127. }
  128. this.migrateDocs();
  129. this.docs.load(this.start.bind(this), this.onBootError.bind(this), {
  130. readCache: true,
  131. writeCache: true,
  132. });
  133. delete this.DOCS;
  134. },
  135. start() {
  136. let doc;
  137. for (doc of Array.from(this.docs.all())) {
  138. this.entries.add(doc.toEntry());
  139. }
  140. for (doc of Array.from(this.disabledDocs.all())) {
  141. this.entries.add(doc.toEntry());
  142. }
  143. for (doc of Array.from(this.docs.all())) {
  144. this.initDoc(doc);
  145. }
  146. this.trigger("ready");
  147. this.router.start();
  148. this.hideLoadingScreen();
  149. setTimeout(() => {
  150. if (!this.doc) {
  151. this.welcomeBack();
  152. }
  153. return this.removeEvent("ready bootError");
  154. }, 50);
  155. },
  156. initDoc(doc) {
  157. for (var type of Array.from(doc.types.all())) {
  158. doc.entries.add(type.toEntry());
  159. }
  160. this.entries.add(doc.entries.all());
  161. },
  162. migrateDocs() {
  163. let needsSaving;
  164. for (var slug of Array.from(this.settings.getDocs())) {
  165. if (!this.docs.findBy("slug", slug)) {
  166. var doc;
  167. needsSaving = true;
  168. if (slug === "webpack~2") {
  169. doc = this.disabledDocs.findBy("slug", "webpack");
  170. }
  171. if (slug === "angular~4_typescript") {
  172. doc = this.disabledDocs.findBy("slug", "angular");
  173. }
  174. if (slug === "angular~2_typescript") {
  175. doc = this.disabledDocs.findBy("slug", "angular~2");
  176. }
  177. if (!doc) {
  178. doc = this.disabledDocs.findBy("slug_without_version", slug);
  179. }
  180. if (doc) {
  181. this.disabledDocs.remove(doc);
  182. this.docs.add(doc);
  183. }
  184. }
  185. }
  186. if (needsSaving) {
  187. this.saveDocs();
  188. }
  189. },
  190. enableDoc(doc, _onSuccess, onError) {
  191. if (this.docs.contains(doc)) {
  192. return;
  193. }
  194. const onSuccess = () => {
  195. if (this.docs.contains(doc)) {
  196. return;
  197. }
  198. this.disabledDocs.remove(doc);
  199. this.docs.add(doc);
  200. this.docs.sort();
  201. this.initDoc(doc);
  202. this.saveDocs();
  203. if (app.settings.get("autoInstall")) {
  204. doc.install(_onSuccess, onError);
  205. } else {
  206. _onSuccess();
  207. }
  208. };
  209. doc.load(onSuccess, onError, { writeCache: true });
  210. },
  211. saveDocs() {
  212. this.settings.setDocs(Array.from(this.docs.all()).map((doc) => doc.slug));
  213. this.db.migrate();
  214. return this.serviceWorker != null
  215. ? this.serviceWorker.updateInBackground()
  216. : undefined;
  217. },
  218. welcomeBack() {
  219. let visitCount = this.settings.get("count");
  220. this.settings.set("count", ++visitCount);
  221. if (visitCount === 5) {
  222. new app.views.Notif("Share", { autoHide: null });
  223. }
  224. new app.views.News();
  225. new app.views.Updates();
  226. return (this.updateChecker = new app.UpdateChecker());
  227. },
  228. reboot() {
  229. if (location.pathname !== "/" && location.pathname !== "/settings") {
  230. window.location = `/#${location.pathname}`;
  231. } else {
  232. window.location = "/";
  233. }
  234. },
  235. reload() {
  236. this.docs.clearCache();
  237. this.disabledDocs.clearCache();
  238. if (this.serviceWorker) {
  239. this.serviceWorker.reload();
  240. } else {
  241. this.reboot();
  242. }
  243. },
  244. reset() {
  245. this.localStorage.reset();
  246. this.settings.reset();
  247. if (this.db != null) {
  248. this.db.reset();
  249. }
  250. if (this.serviceWorker != null) {
  251. this.serviceWorker.update();
  252. }
  253. window.location = "/";
  254. },
  255. showTip(tip) {
  256. if (this.isSingleDoc()) {
  257. return;
  258. }
  259. const tips = this.settings.getTips();
  260. if (tips.indexOf(tip) === -1) {
  261. tips.push(tip);
  262. this.settings.setTips(tips);
  263. new app.views.Tip(tip);
  264. }
  265. },
  266. hideLoadingScreen() {
  267. if ($.overlayScrollbarsEnabled()) {
  268. document.body.classList.add("_overlay-scrollbars");
  269. }
  270. document.documentElement.classList.remove("_booting");
  271. },
  272. indexHost() {
  273. // Can't load the index files from the host/CDN when service worker is
  274. // enabled because it doesn't support caching URLs that use CORS.
  275. return this.config[
  276. this.serviceWorker && this.settings.hasDocs()
  277. ? "index_path"
  278. : "docs_origin"
  279. ];
  280. },
  281. onBootError(...args) {
  282. this.trigger("bootError");
  283. this.hideLoadingScreen();
  284. },
  285. onQuotaExceeded() {
  286. if (this.quotaExceeded) {
  287. return;
  288. }
  289. this.quotaExceeded = true;
  290. new app.views.Notif("QuotaExceeded", { autoHide: null });
  291. },
  292. onCookieBlocked(key, value, actual) {
  293. if (this.cookieBlocked) {
  294. return;
  295. }
  296. this.cookieBlocked = true;
  297. new app.views.Notif("CookieBlocked", { autoHide: null });
  298. Raven.captureMessage(`CookieBlocked/${key}`, {
  299. level: "warning",
  300. extra: { value, actual },
  301. });
  302. },
  303. onWindowError(...args) {
  304. if (this.cookieBlocked) {
  305. return;
  306. }
  307. if (this.isInjectionError(...Array.from(args || []))) {
  308. this.onInjectionError();
  309. } else if (this.isAppError(...Array.from(args || []))) {
  310. if (typeof this.previousErrorHandler === "function") {
  311. this.previousErrorHandler(...Array.from(args || []));
  312. }
  313. this.hideLoadingScreen();
  314. if (!this.errorNotif) {
  315. this.errorNotif = new app.views.Notif("Error");
  316. }
  317. this.errorNotif.show();
  318. }
  319. },
  320. onInjectionError() {
  321. if (!this.injectionError) {
  322. this.injectionError = true;
  323. alert(`\
  324. JavaScript code has been injected in the page which prevents DevDocs from running correctly.
  325. Please check your browser extensions/addons. `);
  326. Raven.captureMessage("injection error", { level: "info" });
  327. }
  328. },
  329. isInjectionError() {
  330. // Some browser extensions expect the entire web to use jQuery.
  331. // I gave up trying to fight back.
  332. return (
  333. window.$ !== app._$ ||
  334. window.$$ !== app._$$ ||
  335. window.page !== app._page ||
  336. typeof $.empty !== "function" ||
  337. typeof page.show !== "function"
  338. );
  339. },
  340. isAppError(error, file) {
  341. // Ignore errors from external scripts.
  342. return (
  343. file &&
  344. file.indexOf("devdocs") !== -1 &&
  345. file.indexOf(".js") === file.length - 3
  346. );
  347. },
  348. isSupportedBrowser() {
  349. try {
  350. const features = {
  351. bind: !!Function.prototype.bind,
  352. pushState: !!history.pushState,
  353. matchMedia: !!window.matchMedia,
  354. insertAdjacentHTML: !!document.body.insertAdjacentHTML,
  355. defaultPrevented:
  356. document.createEvent("CustomEvent").defaultPrevented === false,
  357. cssVariables: !!__guardMethod__(CSS, "supports", (o) =>
  358. o.supports("(--t: 0)"),
  359. ),
  360. };
  361. for (var key in features) {
  362. var value = features[key];
  363. if (!value) {
  364. Raven.captureMessage(`unsupported/${key}`, { level: "info" });
  365. return false;
  366. }
  367. }
  368. return true;
  369. } catch (error) {
  370. Raven.captureMessage("unsupported/exception", {
  371. level: "info",
  372. extra: { error },
  373. });
  374. return false;
  375. }
  376. },
  377. isSingleDoc() {
  378. return document.body.hasAttribute("data-doc");
  379. },
  380. isMobile() {
  381. return this._isMobile != null
  382. ? this._isMobile
  383. : (this._isMobile = app.views.Mobile.detect());
  384. },
  385. isAndroidWebview() {
  386. return this._isAndroidWebview != null
  387. ? this._isAndroidWebview
  388. : (this._isAndroidWebview = app.views.Mobile.detectAndroidWebview());
  389. },
  390. isInvalidLocation() {
  391. return (
  392. this.config.env === "production" &&
  393. location.host.indexOf(app.config.production_host) !== 0
  394. );
  395. },
  396. };
  397. $.extend(app, Events);
  398. function __guard__(value, transform) {
  399. return typeof value !== "undefined" && value !== null
  400. ? transform(value)
  401. : undefined;
  402. }
  403. function __guardMethod__(obj, methodName, transform) {
  404. if (
  405. typeof obj !== "undefined" &&
  406. obj !== null &&
  407. typeof obj[methodName] === "function"
  408. ) {
  409. return transform(obj, methodName);
  410. } else {
  411. return undefined;
  412. }
  413. }