app.js 11 KB

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