app.coffee 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. @app =
  2. _$: $
  3. _$$: $$
  4. _page: page
  5. collections: {}
  6. models: {}
  7. templates: {}
  8. views: {}
  9. init: ->
  10. try @initErrorTracking() catch
  11. return unless @browserCheck()
  12. @el = $('._app')
  13. @localStorage = new LocalStorageStore
  14. @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
  15. @settings = new app.Settings
  16. @db = new app.DB()
  17. @settings.initLayout()
  18. @docs = new app.collections.Docs
  19. @disabledDocs = new app.collections.Docs
  20. @entries = new app.collections.Entries
  21. @router = new app.Router
  22. @shortcuts = new app.Shortcuts
  23. @document = new app.views.Document
  24. @mobile = new app.views.Mobile if @isMobile()
  25. if document.body.hasAttribute('data-doc')
  26. @DOC = JSON.parse(document.body.getAttribute('data-doc'))
  27. @bootOne()
  28. else if @DOCS
  29. @bootAll()
  30. else
  31. @onBootError()
  32. return
  33. browserCheck: ->
  34. return true if @isSupportedBrowser()
  35. document.body.innerHTML = app.templates.unsupportedBrowser
  36. @hideLoadingScreen()
  37. false
  38. initErrorTracking: ->
  39. # Show a warning message and don't track errors when the app is loaded
  40. # from a domain other than our own, because things are likely to break.
  41. # (e.g. cross-domain requests)
  42. if @isInvalidLocation()
  43. new app.views.Notif 'InvalidLocation'
  44. else
  45. if @config.sentry_dsn
  46. Raven.config @config.sentry_dsn,
  47. release: @config.release
  48. whitelistUrls: [/devdocs/]
  49. includePaths: [/devdocs/]
  50. ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/]
  51. tags:
  52. mode: if @isSingleDoc() then 'single' else 'full'
  53. iframe: (window.top isnt window).toString()
  54. electron: (!!window.process?.versions?.electron).toString()
  55. shouldSendCallback: =>
  56. try
  57. if @isInjectionError()
  58. @onInjectionError()
  59. return false
  60. if @isAndroidWebview()
  61. return false
  62. true
  63. dataCallback: (data) ->
  64. try
  65. $.extend(data.user ||= {}, app.settings.dump())
  66. data.user.docs = data.user.docs.split('/') if data.user.docs
  67. data.user.lastIDBTransaction = app.lastIDBTransaction if app.lastIDBTransaction
  68. data.tags.scriptCount = document.scripts.length
  69. data
  70. .install()
  71. @previousErrorHandler = onerror
  72. window.onerror = @onWindowError.bind(@)
  73. CookiesStore.onBlocked = @onCookieBlocked
  74. return
  75. bootOne: ->
  76. @doc = new app.models.Doc @DOC
  77. @docs.reset [@doc]
  78. @doc.load @start.bind(@), @onBootError.bind(@), readCache: true
  79. new app.views.Notice 'singleDoc', @doc
  80. delete @DOC
  81. return
  82. bootAll: ->
  83. docs = @settings.getDocs()
  84. for doc in @DOCS
  85. (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)
  86. @migrateDocs()
  87. @docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
  88. delete @DOCS
  89. return
  90. start: ->
  91. @entries.add doc.toEntry() for doc in @docs.all()
  92. @entries.add doc.toEntry() for doc in @disabledDocs.all()
  93. @initDoc(doc) for doc in @docs.all()
  94. @trigger 'ready'
  95. @router.start()
  96. @hideLoadingScreen()
  97. setTimeout =>
  98. @welcomeBack() unless @doc
  99. @removeEvent 'ready bootError'
  100. , 50
  101. return
  102. initDoc: (doc) ->
  103. doc.entries.add type.toEntry() for type in doc.types.all()
  104. @entries.add doc.entries.all()
  105. return
  106. migrateDocs: ->
  107. for slug in @settings.getDocs() when not @docs.findBy('slug', slug)
  108. needsSaving = true
  109. doc = @disabledDocs.findBy('slug', 'webpack') if slug == 'webpack~2'
  110. doc = @disabledDocs.findBy('slug', 'angular') if slug == 'angular~4_typescript'
  111. doc = @disabledDocs.findBy('slug', 'angular~2') if slug == 'angular~2_typescript'
  112. doc ||= @disabledDocs.findBy('slug_without_version', slug)
  113. if doc
  114. @disabledDocs.remove(doc)
  115. @docs.add(doc)
  116. @saveDocs() if needsSaving
  117. return
  118. enableDoc: (doc, _onSuccess, onError) ->
  119. return if @docs.contains(doc)
  120. onSuccess = =>
  121. return if @docs.contains(doc)
  122. @disabledDocs.remove(doc)
  123. @docs.add(doc)
  124. @docs.sort()
  125. @initDoc(doc)
  126. @saveDocs()
  127. if app.settings.get('autoInstall')
  128. doc.install(_onSuccess, onError)
  129. else
  130. _onSuccess()
  131. return
  132. doc.load onSuccess, onError, writeCache: true
  133. return
  134. saveDocs: ->
  135. @settings.setDocs(doc.slug for doc in @docs.all())
  136. @db.migrate()
  137. @serviceWorker?.updateInBackground()
  138. welcomeBack: ->
  139. visitCount = @settings.get('count')
  140. @settings.set 'count', ++visitCount
  141. new app.views.Notif 'Share', autoHide: null if visitCount is 5
  142. new app.views.News()
  143. new app.views.Updates()
  144. @updateChecker = new app.UpdateChecker()
  145. reboot: ->
  146. if location.pathname isnt '/' and location.pathname isnt '/settings'
  147. window.location = "/##{location.pathname}"
  148. else
  149. window.location = '/'
  150. return
  151. reload: ->
  152. @docs.clearCache()
  153. @disabledDocs.clearCache()
  154. if @serviceWorker then @serviceWorker.reload() else @reboot()
  155. return
  156. reset: ->
  157. @localStorage.reset()
  158. @settings.reset()
  159. @db?.reset()
  160. @serviceWorker?.update()
  161. window.location = '/'
  162. return
  163. showTip: (tip) ->
  164. return if @isSingleDoc()
  165. tips = @settings.getTips()
  166. if tips.indexOf(tip) is -1
  167. tips.push(tip)
  168. @settings.setTips(tips)
  169. new app.views.Tip(tip)
  170. return
  171. hideLoadingScreen: ->
  172. document.body.classList.add '_overlay-scrollbars' if $.overlayScrollbarsEnabled()
  173. document.documentElement.classList.remove '_booting'
  174. return
  175. indexHost: ->
  176. # Can't load the index files from the host/CDN when service worker is
  177. # enabled because it doesn't support caching URLs that use CORS.
  178. @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']
  179. onBootError: (args...) ->
  180. @trigger 'bootError'
  181. @hideLoadingScreen()
  182. return
  183. onQuotaExceeded: ->
  184. return if @quotaExceeded
  185. @quotaExceeded = true
  186. new app.views.Notif 'QuotaExceeded', autoHide: null
  187. return
  188. onCookieBlocked: (key, value, actual) ->
  189. return if @cookieBlocked
  190. @cookieBlocked = true
  191. new app.views.Notif 'CookieBlocked', autoHide: null
  192. Raven.captureMessage "CookieBlocked/#{key}", level: 'warning', extra: {value, actual}
  193. return
  194. onWindowError: (args...) ->
  195. return if @cookieBlocked
  196. if @isInjectionError args...
  197. @onInjectionError()
  198. else if @isAppError args...
  199. @previousErrorHandler? args...
  200. @hideLoadingScreen()
  201. @errorNotif or= new app.views.Notif 'Error'
  202. @errorNotif.show()
  203. return
  204. onInjectionError: ->
  205. unless @injectionError
  206. @injectionError = true
  207. alert """
  208. JavaScript code has been injected in the page which prevents DevDocs from running correctly.
  209. Please check your browser extensions/addons. """
  210. Raven.captureMessage 'injection error', level: 'info'
  211. return
  212. isInjectionError: ->
  213. # Some browser extensions expect the entire web to use jQuery.
  214. # I gave up trying to fight back.
  215. window.$ isnt app._$ or window.$$ isnt app._$$ or window.page isnt app._page or typeof $.empty isnt 'function' or typeof page.show isnt 'function'
  216. isAppError: (error, file) ->
  217. # Ignore errors from external scripts.
  218. file and file.indexOf('devdocs') isnt -1 and file.indexOf('.js') is file.length - 3
  219. isSupportedBrowser: ->
  220. try
  221. features =
  222. bind: !!Function::bind
  223. pushState: !!history.pushState
  224. matchMedia: !!window.matchMedia
  225. insertAdjacentHTML: !!document.body.insertAdjacentHTML
  226. defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false
  227. cssVariables: !!CSS?.supports?('(--t: 0)')
  228. for key, value of features when not value
  229. Raven.captureMessage "unsupported/#{key}", level: 'info'
  230. return false
  231. true
  232. catch error
  233. Raven.captureMessage 'unsupported/exception', level: 'info', extra: { error: error }
  234. false
  235. isSingleDoc: ->
  236. document.body.hasAttribute('data-doc')
  237. isMobile: ->
  238. @_isMobile ?= app.views.Mobile.detect()
  239. isAndroidWebview: ->
  240. @_isAndroidWebview ?= app.views.Mobile.detectAndroidWebview()
  241. isInvalidLocation: ->
  242. @config.env is 'production' and location.host.indexOf(app.config.production_host) isnt 0
  243. $.extend app, Events