From 3798398b9d1d0535cb50cc5bc1f7c463f77db5a6 Mon Sep 17 00:00:00 2001 From: Victor Costan Date: Sun, 13 Sep 2020 21:26:19 -0700 Subject: [PATCH 1/4] Add option to make storage persistent to Offline page. Fixes #703 --- assets/javascripts/templates/error_tmpl.js | 8 ++++ .../templates/pages/offline_tmpl.js | 26 ++++++++++- .../javascripts/views/content/offline_page.js | 46 ++++++++++++++++--- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/assets/javascripts/templates/error_tmpl.js b/assets/javascripts/templates/error_tmpl.js index 7f96247382..c8a334ab33 100644 --- a/assets/javascripts/templates/error_tmpl.js +++ b/assets/javascripts/templates/error_tmpl.js @@ -73,6 +73,14 @@ This could be because you're browsing in private mode or have disallowed offline return error("Offline mode is unavailable.", reason); }; +app.templates.persistenceError = function (exception) { + const reason = exception + ? `${exception.name}: ${exception.message}` + : "Bookmark this site and try again."; + + return error("Persistence request denied by browser.", reason); +}; + app.templates.unsupportedBrowser = `\

Your browser is unsupported, sorry.

diff --git a/assets/javascripts/templates/pages/offline_tmpl.js b/assets/javascripts/templates/pages/offline_tmpl.js index ad46753195..93da0309b1 100644 --- a/assets/javascripts/templates/pages/offline_tmpl.js +++ b/assets/javascripts/templates/pages/offline_tmpl.js @@ -1,4 +1,4 @@ -app.templates.offlinePage = (docs) => `\ +app.templates.offlinePage = (docs, hasPersistence, isPersistent) => `\

Offline Documentation

@@ -23,7 +23,10 @@ app.templates.offlinePage = (docs) => `\ ${docs}
-

Note: your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there. +

+
+ ${offlinePersistenceNote(hasPersistence, isPersistent)} +

Questions & Answers

How does this work? @@ -42,6 +45,25 @@ app.templates.offlinePage = (docs) => `\
\ `; +var offlinePersistenceNote = function (hasPersistence, isPersistent) { + if (isPersistent) { + return ""; + } + + let html = + "

Note: your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while."; + + if (hasPersistence) { + html += + ' .'; + } else { + html += + " Load this page before going offline to make sure the data is still there."; + } + + return html; +}; + var canICloseTheTab = function () { if (app.ServiceWorker.isEnabled()) { return ' Yes! Even offline, you can open a new tab, go to devdocs.io, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). '; diff --git a/assets/javascripts/views/content/offline_page.js b/assets/javascripts/views/content/offline_page.js index 5fe4f21d86..58985f0dd4 100644 --- a/assets/javascripts/views/content/offline_page.js +++ b/assets/javascripts/views/content/offline_page.js @@ -25,12 +25,16 @@ app.views.OfflinePage = class OfflinePage extends app.View { if (statuses === false) { this.html(this.tmpl("offlineError", app.db.reason, app.db.error)); } else { - let html = ""; - for (var doc of app.docs.all()) { - html += this.renderDoc(doc, statuses[doc.slug]); - } - this.html(this.tmpl("offlinePage", html)); - this.refreshLinks(); + this.checkPersistence((hasPersistence, isPersistent) => { + let html = ""; + for (var doc of app.docs.all()) { + html += this.renderDoc(doc, statuses[doc.slug]); + } + this.html( + this.tmpl("offlinePage", html, hasPersistence, isPersistent) + ); + this.refreshLinks(); + }); } }); } @@ -93,6 +97,8 @@ app.views.OfflinePage = class OfflinePage extends app.View { for (el of Array.from(this.findAll(`[data-action='${action}']`))) { $.click(el); } + } else if (el.hasAttribute("data-enable-persistence")) { + this.requestPersistence(); } } @@ -142,4 +148,32 @@ app.views.OfflinePage = class OfflinePage extends app.View { app.settings.set("manualUpdate", !event.target.checked); } } + + checkPersistence(callback) { + if (navigator.storage && navigator.storage.persisted) { + navigator.storage + .persisted() + .then((persisted) => callback(true, persisted)) + .catch(() => callback(false, persisted)); + } else { + callback(false, false); + } + } + + requestPersistence() { + navigator.storage + .persist() + .then((success) => this.onPersistenceRequestCompleted(success)) + .catch((exception) => + this.onPersistenceRequestCompleted(false, exception) + ); + } + + onPersistenceRequestCompleted(success, exception) { + if (success) { + this.render(); + } else { + this.html(this.tmpl("persistenceError", exception)); + } + } }; From 4063dc34307f6d00639514e7055e65e8dc4d3aa7 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Mon, 14 Sep 2026 08:39:12 +0200 Subject: [PATCH 2/4] Fix defects in the offline persistence option The preceding commit was transliterated from CoffeeScript, which main has since converted to JavaScript. These three defects come from the original and are fixed here rather than silently during the port: * the persistence note added one more than it opened * checkPersistence's catch handler passed `persisted`, which is only bound inside the then handler, so a rejected persisted() raised a ReferenceError instead of reporting that persistence is unavailable * the button's class attribute was written `class =` --- assets/javascripts/templates/pages/offline_tmpl.js | 3 +-- assets/javascripts/views/content/offline_page.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/templates/pages/offline_tmpl.js b/assets/javascripts/templates/pages/offline_tmpl.js index 93da0309b1..681b1bd8e6 100644 --- a/assets/javascripts/templates/pages/offline_tmpl.js +++ b/assets/javascripts/templates/pages/offline_tmpl.js @@ -23,7 +23,6 @@ app.templates.offlinePage = (docs, hasPersistence, isPersistent) => `\ ${docs} -

${offlinePersistenceNote(hasPersistence, isPersistent)}
@@ -55,7 +54,7 @@ var offlinePersistenceNote = function (hasPersistence, isPersistent) { if (hasPersistence) { html += - ' .'; + ' .'; } else { html += " Load this page before going offline to make sure the data is still there."; diff --git a/assets/javascripts/views/content/offline_page.js b/assets/javascripts/views/content/offline_page.js index 58985f0dd4..174f24009c 100644 --- a/assets/javascripts/views/content/offline_page.js +++ b/assets/javascripts/views/content/offline_page.js @@ -154,7 +154,7 @@ app.views.OfflinePage = class OfflinePage extends app.View { navigator.storage .persisted() .then((persisted) => callback(true, persisted)) - .catch(() => callback(false, persisted)); + .catch(() => callback(false, false)); } else { callback(false, false); } From 62fcda5e9dfb29d578d4be6ce2295db94cc4c414 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Mon, 14 Sep 2026 08:39:39 +0200 Subject: [PATCH 3/4] Report a denied persistence request inside the offline page navigator.storage.persist() resolves false without raising whenever the browser declines -- Chrome when its heuristics are unmet, Firefox when the prompt is dismissed -- so the denial path is one users hit in the course of normal use, not an exceptional one. Rendering it through the page-level error template replaced the whole view, discarding the documentation table, so a declined request left no way back other than navigating away and returning. Write the outcome to the #_offline-persistence-note container the note already carries an id for, and style the message as an inline note: ._error is centred with position: absolute and cannot be nested. --- assets/javascripts/templates/error_tmpl.js | 8 -------- assets/javascripts/templates/pages/offline_tmpl.js | 8 ++++++++ assets/javascripts/views/content/offline_page.js | 10 ++++++---- assets/stylesheets/components/_content.scss | 1 + 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/assets/javascripts/templates/error_tmpl.js b/assets/javascripts/templates/error_tmpl.js index c8a334ab33..7f96247382 100644 --- a/assets/javascripts/templates/error_tmpl.js +++ b/assets/javascripts/templates/error_tmpl.js @@ -73,14 +73,6 @@ This could be because you're browsing in private mode or have disallowed offline return error("Offline mode is unavailable.", reason); }; -app.templates.persistenceError = function (exception) { - const reason = exception - ? `${exception.name}: ${exception.message}` - : "Bookmark this site and try again."; - - return error("Persistence request denied by browser.", reason); -}; - app.templates.unsupportedBrowser = `\

Your browser is unsupported, sorry.

diff --git a/assets/javascripts/templates/pages/offline_tmpl.js b/assets/javascripts/templates/pages/offline_tmpl.js index 681b1bd8e6..0ecac8c3ea 100644 --- a/assets/javascripts/templates/pages/offline_tmpl.js +++ b/assets/javascripts/templates/pages/offline_tmpl.js @@ -44,6 +44,14 @@ app.templates.offlinePage = (docs, hasPersistence, isPersistent) => `\ \ `; +app.templates.persistenceError = function (exception) { + const reason = exception + ? `${exception.name}: ${exception.message}` + : "Bookmark this site and try again."; + + return `

Persistent storage was denied by your browser. ${reason}`; +}; + var offlinePersistenceNote = function (hasPersistence, isPersistent) { if (isPersistent) { return ""; diff --git a/assets/javascripts/views/content/offline_page.js b/assets/javascripts/views/content/offline_page.js index 174f24009c..652f449500 100644 --- a/assets/javascripts/views/content/offline_page.js +++ b/assets/javascripts/views/content/offline_page.js @@ -170,10 +170,12 @@ app.views.OfflinePage = class OfflinePage extends app.View { } onPersistenceRequestCompleted(success, exception) { - if (success) { - this.render(); - } else { - this.html(this.tmpl("persistenceError", exception)); + const note = this.find("#_offline-persistence-note"); + if (!note) { + return; } + // Granting persistence retires the note, which is what a fresh render of + // the page would produce; the disappearing button is the confirmation. + note.innerHTML = success ? "" : this.tmpl("persistenceError", exception); } }; diff --git a/assets/stylesheets/components/_content.scss b/assets/stylesheets/components/_content.scss index b25956e6d2..e4dd033f37 100644 --- a/assets/stylesheets/components/_content.scss +++ b/assets/stylesheets/components/_content.scss @@ -385,6 +385,7 @@ ._bold { font-weight: var(--boldFontWeight); } ._note { @extend %note; } ._note-green { @extend %note-green; } +._note-red { @extend %note-red; } ._label { @extend %label; } ._code { @extend %code; } ._highlight, ._highlight > td { background: var(--highlightBackground) !important; } From 853b493e0127538c3bc48056abc5a56bff6a22f2 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Mon, 14 Sep 2026 08:39:43 +0200 Subject: [PATCH 4/4] Skip rendering the offline page into a deactivated view Querying persistence made render() finish asynchronously, so the this.activated check it performs beforehand no longer holds by the time the page is written. Re-check it in both callbacks, as the surrounding install callbacks already do. --- assets/javascripts/views/content/offline_page.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assets/javascripts/views/content/offline_page.js b/assets/javascripts/views/content/offline_page.js index 652f449500..10852485d9 100644 --- a/assets/javascripts/views/content/offline_page.js +++ b/assets/javascripts/views/content/offline_page.js @@ -26,6 +26,9 @@ app.views.OfflinePage = class OfflinePage extends app.View { this.html(this.tmpl("offlineError", app.db.reason, app.db.error)); } else { this.checkPersistence((hasPersistence, isPersistent) => { + if (!this.activated) { + return; + } let html = ""; for (var doc of app.docs.all()) { html += this.renderDoc(doc, statuses[doc.slug]); @@ -170,6 +173,9 @@ app.views.OfflinePage = class OfflinePage extends app.View { } onPersistenceRequestCompleted(success, exception) { + if (!this.activated) { + return; + } const note = this.find("#_offline-persistence-note"); if (!note) { return;