History

Add ability to cache complete documntations in IndexedDB

Thibaut committed 11 years ago

Commit 25f844d1 parent bc5488f11 files · +302 −14Browse files

Unified · Split

11 files
  1. assets/javascripts/app/app.coffee +9 −4
  2. assets/javascripts/app/config.coffee.erb +1 −0
  3. assets/javascripts/app/db.coffee +115 −0
  4. assets/javascripts/app/router.coffee +5 −0
  5. assets/javascripts/models/doc.coffee +39 −0
  6. assets/javascripts/models/entry.coffee +1 −5
  7. assets/javascripts/templates/pages/offline_tmpl.coffee +16 −0
  8. assets/javascripts/views/content/content.coffee +7 −4
  9. assets/javascripts/views/content/offline_page.coffee +63 −0
  10. assets/stylesheets/components/_content.scss +45 −0
  11. lib/app.rb +1 −1
assets/javascripts/app/app.coffee+9−4
@@ -61,7 +61,7 @@ · 60 unmodified lines
61 bootOne: ->61 bootOne: ->
62 @doc = new app.models.Doc @DOC62 @doc = new app.models.Doc @DOC
63 @docs.reset [@doc]63 @docs.reset [@doc]
64 @doc.load @start.bind(@), @onBootError.bind(@), readCache: true64 @doc.load @bootDB.bind(@), @onBootError.bind(@), readCache: true
65 new app.views.Notice 'singleDoc', @doc65 new app.views.Notice 'singleDoc', @doc
66 delete @DOC66 delete @DOC
67 return67 return
@@ -72,20 +72,25 @@ · 4 unmodified lines
72 (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)72 (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)
73 @docs.sort()73 @docs.sort()
74 @disabledDocs.sort()74 @disabledDocs.sort()
75 @docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true75 @docs.load @bootDB.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
76 delete @DOCS76 delete @DOCS
77 return77 return
78 78
79 start: ->79 bootDB: ->
80 for doc in @docs.all()80 for doc in @docs.all()
81 @entries.add doc.toEntry()81 @entries.add doc.toEntry()
82 @entries.add type.toEntry() for type in doc.types.all()82 @entries.add type.toEntry() for type in doc.types.all()
83 @entries.add doc.entries.all()83 @entries.add doc.entries.all()
84
85 @db = new app.DB()
86 @db.init(@start.bind(@))
87 return
88
89 start: ->
84 @trigger 'ready'90 @trigger 'ready'
85 @router.start()91 @router.start()
86 @hideLoading()92 @hideLoading()
87 @welcomeBack() unless @doc93 @welcomeBack() unless @doc
88
89 @removeEvent 'ready bootError'94 @removeEvent 'ready bootError'
90 return95 return
91 96
assets/javascripts/app/config.coffee.erb+1−0
@@ -8,3 +8,4 @@ app.config = · 7 unmodified lines
8 production_host: 'devdocs.io'8 production_host: 'devdocs.io'
9 search_param: 'q'9 search_param: 'q'
10 sentry_dsn: '<%= App.sentry_dsn %>'10 sentry_dsn: '<%= App.sentry_dsn %>'
11 version: '<%= Time.now.to_i %>'
assets/javascripts/app/db.coffee+115−0
@@ -0,0 +1,115 @@
1class app.DB
2 NAME = 'docs'
3
4 constructor: ->
5 @useIndexedDB = @useIndexedDB()
6
7 init: (@_callback) ->
8 if @useIndexedDB
9 @initIndexedDB()
10 else
11 @callback()
12 return
13
14 initIndexedDB: ->
15 try
16 req = indexedDB.open(NAME, @indexedDBVersion())
17 req.onerror = @callback
18 req.onsuccess = @onOpenSuccess
19 req.onupgradeneeded = @onUpgradeNeeded
20 catch
21 @callback()
22 return
23
24 isEnabled: ->
25 !!@db
26
27 callback: =>
28 @_callback?()
29 @_callback = null
30 return
31
32 onOpenSuccess: (event) =>
33 try
34 @db = event.target.result
35 @db.transaction(['docs', app.docs.all()[0].slug], 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937
36 catch
37 @db = null
38
39 @callback()
40 return
41
42 onUpgradeNeeded: (event) =>
43 db = event.target.result
44
45 unless db.objectStoreNames.contains('docs')
46 db.createObjectStore('docs')
47
48 for doc in app.docs.all() when not db.objectStoreNames.contains(doc.slug)
49 db.createObjectStore(doc.slug)
50
51 for doc in app.disabledDocs.all() when not db.objectStoreNames.contains(doc.slug)
52 db.createObjectStore(doc.slug)
53 return
54
55 store: (doc, data, onSuccess, onError) ->
56 txn = @db.transaction ['docs', doc.slug], 'readwrite'
57 txn.oncomplete = -> if txn.error then onError() else onSuccess()
58
59 store = txn.objectStore(doc.slug)
60 store.clear()
61 store.add(content, path) for path, content of data
62
63 store = txn.objectStore('docs')
64 store.put(doc.mtime, doc.slug)
65 return
66
67 unstore: (doc, onSuccess, onError) ->
68 txn = @db.transaction ['docs', doc.slug], 'readwrite'
69 txn.oncomplete = -> if txn.error then onError() else onSuccess()
70
71 store = txn.objectStore(doc.slug)
72 store.clear()
73
74 store = txn.objectStore('docs')
75 store.delete(doc.slug)
76 return
77
78 version: (doc, callback) ->
79 txn = @db.transaction ['docs'], 'readonly'
80 store = txn.objectStore('docs')
81
82 req = store.get(doc.slug)
83 req.onsuccess = -> callback(!!req.result)
84 req.onerror = -> callback(false)
85 return
86
87 load: (entry, onSuccess, onError) ->
88 if @isEnabled()
89 onError = @loadWithXHR.bind(@, entry, onSuccess, onError)
90 @loadWithIDB(entry, onSuccess, onError)
91 else
92 @loadWithXHR(entry, onSuccess, onError)
93
94 loadWithXHR: (entry, onSuccess, onError) ->
95 ajax
96 url: entry.fileUrl()
97 dataType: 'html'
98 success: onSuccess
99 error: onError
100
101 loadWithIDB: (entry, onSuccess, onError) ->
102 txn = @db.transaction [entry.doc.slug], 'readonly'
103 store = txn.objectStore(entry.doc.slug)
104
105 req = store.get(entry.path)
106 req.onsuccess = -> if req.result then onSuccess(req.result) else onError()
107 req.onerror = onError
108
109 txn
110
111 useIndexedDB: ->
112 !app.isSingleDoc() and !!window.indexedDB
113
114 indexedDBVersion: ->
115 if app.config.env is 'production' then app.config.version else Date.now() / 1000
assets/javascripts/app/router.coffee+5−0
@@ -4,6 +4,7 @@ class app.Router · 3 unmodified lines
4 @routes: [4 @routes: [
5 ['*', 'before' ]5 ['*', 'before' ]
6 ['/', 'root' ]6 ['/', 'root' ]
7 ['/offline', 'offline' ]
7 ['/about', 'about' ]8 ['/about', 'about' ]
8 ['/news', 'news' ]9 ['/news', 'news' ]
9 ['/help', 'help' ]10 ['/help', 'help' ]
@@ -75,6 +76,10 @@ class app.Router · 65 unmodified lines
75 @triggerRoute 'root'76 @triggerRoute 'root'
76 return77 return
77 78
79 offline: ->
80 @triggerRoute 'offline'
81 return
82
78 about: (context) ->83 about: (context) ->
79 context.page = 'about'84 context.page = 'about'
80 @triggerRoute 'page'85 @triggerRoute 'page'
assets/javascripts/models/doc.coffee+39−0
@@ -27,6 +27,9 @@ class app.models.Doc extends app.Model · 26 unmodified lines
27 fileUrl: (path) ->27 fileUrl: (path) ->
28 "#{app.config.docs_host}#{@fullPath(path)}"28 "#{app.config.docs_host}#{@fullPath(path)}"
29 29
30 dbUrl: ->
31 "#{app.config.docs_host}/#{@db_path}?#{@mtime}"
32
30 indexUrl: ->33 indexUrl: ->
31 "#{app.indexHost()}/#{@index_path}?#{@mtime}"34 "#{app.indexHost()}/#{@index_path}?#{@mtime}"
32 35
@@ -83,3 +86,39 @@ class app.models.Doc extends app.Model · 50 unmodified lines
83 _setCache: (data) ->86 _setCache: (data) ->
84 app.store.set @slug, [@mtime, data]87 app.store.set @slug, [@mtime, data]
85 return88 return
89
90 download: (onSuccess, onError) ->
91 return if @downloading
92 @downloading = true
93
94 error = =>
95 @downloading = null
96 onError()
97
98 success = (data) =>
99 @downloading = null
100 app.db.store @, data, onSuccess, error
101
102 ajax
103 url: @dbUrl()
104 success: success
105 error: error
106 return
107
108 undownload: (onSuccess, onError) ->
109 return if @downloading
110 @downloading = true
111
112 success = =>
113 @downloading = null
114 onSuccess()
115
116 error = =>
117 @downloading = null
118 onError()
119
120 app.db.unstore @, success, error
121
122 getDownloadStatus: (callback) ->
123 app.db.version @, (value) ->
124 callback downloaded: !!value, version: value
assets/javascripts/models/entry.coffee+1−5
@@ -41,8 +41,4 @@ class app.models.Entry extends app.Model · 40 unmodified lines
41 @doc.types.findBy 'name', @type41 @doc.types.findBy 'name', @type
42 42
43 loadFile: (onSuccess, onError) ->43 loadFile: (onSuccess, onError) ->
44 ajax44 app.db.load(@, onSuccess, onError)
45 url: @fileUrl()
46 dataType: 'html'
47 success: onSuccess
48 error: onError
assets/javascripts/templates/pages/offline_tmpl.coffee+16−0
@@ -0,0 +1,16 @@
1app.templates.offlinePage = ->
2 """ <h1 class="_lined-heading">Offline</h1>
3 <table class="_docs">
4 #{app.templates.render 'offlineDoc', app.docs.all()}
5 </table> """
6
7app.templates.offlineDoc = (doc) ->
8 """<tr data-slug="#{doc.slug}"></tr>"""
9
10app.templates.offlineDocContent = (doc, status) ->
11 html = """<th class="_icon-#{doc.slug}">#{doc.name}</th>"""
12 html += if status.downloaded
13 """<td><a data-del>Delete</a></td>"""
14 else
15 """<td><a data-dl>Download</a></td>"""
16 html
assets/javascripts/views/content/content.coffee+7−4
@@ -23,10 +23,11 @@ class app.views.Content extends app.View · 22 unmodified lines
23 @scrollMap = {}23 @scrollMap = {}
24 @scrollStack = []24 @scrollStack = []
25 25
26 @rootPage = new app.views.RootPage26 @rootPage = new app.views.RootPage
27 @staticPage = new app.views.StaticPage27 @staticPage = new app.views.StaticPage
28 @typePage = new app.views.TypePage28 @offlinePage = new app.views.OfflinePage
29 @entryPage = new app.views.EntryPage29 @typePage = new app.views.TypePage
30 @entryPage = new app.views.EntryPage
30 31
31 @entryPage32 @entryPage
32 .on 'loading', @onEntryLoading33 .on 'loading', @onEntryLoading
@@ -137,6 +138,8 @@ class app.views.Content extends app.View · 104 unmodified lines
137 @show @entryPage138 @show @entryPage
138 when 'type'139 when 'type'
139 @show @typePage140 @show @typePage
141 when 'offline'
142 @show @offlinePage
140 else143 else
141 @show @staticPage144 @show @staticPage
142 145
assets/javascripts/views/content/offline_page.coffee+63−0
@@ -0,0 +1,63 @@
1class app.views.OfflinePage extends app.View
2 @className: '_static'
3
4 @events:
5 click: 'onClick'
6
7 @elements:
8 list: '_._docs'
9
10 deactivate: ->
11 if super
12 @empty()
13 return
14
15 render: ->
16 @html @tmpl('offlinePage')
17 @refreshElements()
18 app.docs.each(@renderDoc)
19 return
20
21 renderDoc: (doc) =>
22 doc.getDownloadStatus (status) =>
23 html = app.templates.render('offlineDocContent', doc, status)
24 el = @docEl(doc)
25 el.className = ''
26 el.innerHTML = html
27 return
28
29 getTitle: ->
30 'Offline'
31
32 getDoc: (el) ->
33 el = el.parentNode until slug = el.getAttribute('data-slug')
34 app.docs.findBy('slug', slug)
35
36 docEl: (doc) ->
37 @find("[data-slug='#{doc.slug}']")
38
39 onRoute: ->
40 @render()
41 return
42
43 onClick: (event) =>
44 if event.target.hasAttribute('data-dl')
45 action = 'download'
46 else if event.target.hasAttribute('data-del')
47 action = 'undownload'
48
49 if action
50 $.stopEvent(event)
51 doc = @getDoc(event.target)
52 doc[action](@onDownloadSuccess.bind(@, doc), @onDownloadError.bind(@, doc))
53 @docEl(doc).classList.add("#{action}ing")
54 return
55
56 onDownloadSuccess: (doc) ->
57 @renderDoc(doc)
58 return
59
60 onDownloadError: (doc) ->
61 el = @docEl(doc)
62 el.className = ''
63 el.classList.add('error')
assets/stylesheets/components/_content.scss+45−0
@@ -242,6 +242,51 @@ · 241 unmodified lines
242 td:first-child, td:last-child { white-space: nowrap; }242 td:first-child, td:last-child { white-space: nowrap; }
243}243}
244 244
245//
246// Doc table
247//
248
249._docs {
250 width: 100%;
251 line-height: 1.5rem;
252
253 th {
254 max-width: 0;
255 padding-left: .5rem;
256 padding-right: .5rem;
257 white-space: nowrap;
258 font-weight: normal;
259
260 &:before {
261 float: left;
262 margin: .25rem .5rem .25rem 0;
263 @extend %icon;
264 }
265 }
266
267 td:last-child { text-align: right; }
268 td > a { cursor: pointer; }
269
270 tr.downloading > td:last-child {
271 > a { display: none; }
272 &:before { content: 'Downloading…' }
273 }
274
275 tr.undownloading > td:last-child {
276 > a { display: none; }
277 &:before { content: 'Deleting…' }
278 }
279
280 tr.error > td:last-child {
281 > a { display: none; }
282
283 &:before {
284 content: 'Error';
285 color: red;
286 }
287 }
288}
289
245//290//
246// News291// News
247//292//
lib/app.rb+1−1
@@ -122,7 +122,7 @@ class App < Sinatra::Application · 121 unmodified lines
122 erb :index122 erb :index
123 end123 end
124 124
125 %w(about news help).each do |page|125 %w(offline about news help).each do |page|
126 get "/#{page}" do126 get "/#{page}" do
127 redirect "/#/#{page}", 302127 redirect "/#/#{page}", 302
128 end128 end