From e516c9bfd2a3850a1e3c7b58eb24734e539c399c Mon Sep 17 00:00:00 2001 From: Ryan Gabriel Date: Mon, 24 Aug 2026 00:20:16 +0800 Subject: [PATCH 1/5] Fix silent failures of Alt+O / Alt+C shortcuts Alt+O (open original page) and Alt+C (copy original link) both bail out silently when the rendered entry has no ._attribution ._attribution-link element, which left users of affected documentations with dead shortcuts and no feedback (#2634). Alt+C also logged to console on every use and ignored clipboard promise rejections after the navigator.clipboard migration. - Show a transient notice when no original-page link exists - Surface a notice if the clipboard write rejects instead of failing silently; drop the leftover console.log - Notices auto-dismiss after 3s without stacking --- assets/javascripts/templates/notice_tmpl.js | 6 ++++++ .../javascripts/views/content/entry_page.js | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/assets/javascripts/templates/notice_tmpl.js b/assets/javascripts/templates/notice_tmpl.js index 49793eb571..26c8c947b1 100644 --- a/assets/javascripts/templates/notice_tmpl.js +++ b/assets/javascripts/templates/notice_tmpl.js @@ -7,3 +7,9 @@ app.templates.singleDocNotice = (doc) => app.templates.disabledDocNotice = () => notice(` This documentation is disabled. To enable it, go to Preferences. `); + +app.templates.noOriginalLinkNotice = () => + notice(` The original page link is not available for this documentation. `); + +app.templates.copyFailedNotice = () => + notice(` Couldn't copy the original page link to the clipboard. `); diff --git a/assets/javascripts/views/content/entry_page.js b/assets/javascripts/views/content/entry_page.js index 961d90e1fa..8b7bc4dd52 100644 --- a/assets/javascripts/views/content/entry_page.js +++ b/assets/javascripts/views/content/entry_page.js @@ -221,17 +221,32 @@ app.views.EntryPage = class EntryPage extends app.View { onAltC() { const link = this.find("._attribution:last-child ._attribution-link"); if (!link) { + this.showTransientNotice("noOriginalLink"); return; } - console.log(link.href + location.hash); - navigator.clipboard.writeText(link.href + location.hash); + navigator.clipboard.writeText(link.href + location.hash).catch(() => + this.showTransientNotice("copyFailed"), + ); } onAltO() { const link = this.find("._attribution:last-child ._attribution-link"); if (!link) { + this.showTransientNotice("noOriginalLink"); return; } this.delay(() => $.popup(link.href + location.hash)); } + + showTransientNotice(type) { + if (this.transientNotice) { + clearTimeout(this.transientNoticeTimer); + this.transientNotice.deactivate(); + } + this.transientNotice = new app.views.Notice(type); + this.transientNoticeTimer = setTimeout(() => { + this.transientNotice.deactivate(); + this.transientNotice = null; + }, 3000); + } }; From 8c023b1a7553486c55dadbe8d7d52ef6d81eb8b2 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sun, 13 Sep 2026 13:20:16 +0200 Subject: [PATCH 2/5] Find the original page link independently of sibling position The `._attribution:last-child` lookup failed whenever the attribution was followed by another element, so Alt+O / Alt+C reported the link as unavailable even though it existed. Match the last attribution link instead, which keeps the previous intent (the appended attribution wins over any inside the page content) without depending on sibling position. --- assets/javascripts/views/content/entry_page.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/assets/javascripts/views/content/entry_page.js b/assets/javascripts/views/content/entry_page.js index 8b7bc4dd52..528ad354ec 100644 --- a/assets/javascripts/views/content/entry_page.js +++ b/assets/javascripts/views/content/entry_page.js @@ -218,8 +218,15 @@ app.views.EntryPage = class EntryPage extends app.View { } } + originalLink() { + // The attribution is appended last but may be followed by other elements, + // so match on the last attribution rather than on its sibling position. + const links = this.findAll("._attribution ._attribution-link"); + return links[links.length - 1]; + } + onAltC() { - const link = this.find("._attribution:last-child ._attribution-link"); + const link = this.originalLink(); if (!link) { this.showTransientNotice("noOriginalLink"); return; @@ -230,7 +237,7 @@ app.views.EntryPage = class EntryPage extends app.View { } onAltO() { - const link = this.find("._attribution:last-child ._attribution-link"); + const link = this.originalLink(); if (!link) { this.showTransientNotice("noOriginalLink"); return; From 5987872d5bfe0dd03249b9893f0e392e7e319416 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sun, 13 Sep 2026 13:20:21 +0200 Subject: [PATCH 3/5] Notify when the Clipboard API is unavailable `navigator.clipboard` is undefined in unsupported or non-secure browser contexts, so Alt+C threw before the rejection handler could report the failure. Check for the API up front and show the same notice. --- assets/javascripts/views/content/entry_page.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/views/content/entry_page.js b/assets/javascripts/views/content/entry_page.js index 528ad354ec..938aa04fae 100644 --- a/assets/javascripts/views/content/entry_page.js +++ b/assets/javascripts/views/content/entry_page.js @@ -231,9 +231,13 @@ app.views.EntryPage = class EntryPage extends app.View { this.showTransientNotice("noOriginalLink"); return; } - navigator.clipboard.writeText(link.href + location.hash).catch(() => - this.showTransientNotice("copyFailed"), - ); + if (!navigator.clipboard) { + this.showTransientNotice("copyFailed"); + return; + } + navigator.clipboard + .writeText(link.href + location.hash) + .catch(() => this.showTransientNotice("copyFailed")); } onAltO() { From c9fe0f0c09d691df1abc8a107f892e3ff47a8cd2 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sun, 13 Sep 2026 13:20:27 +0200 Subject: [PATCH 4/5] Keep the transient notice above persistent notices All notices share the same absolute bounds and z-index, and `Notice.show()` prepends, so the transient notice was painted over by an existing single-doc or disabled-doc notice. Raise it with a `_notice-transient` class, and route every teardown through `hideTransientNotice()` so the notice can't outlive the page it was shown on. --- .../javascripts/views/content/entry_page.js | 24 ++++++++++++------- assets/stylesheets/components/_notice.scss | 2 ++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/assets/javascripts/views/content/entry_page.js b/assets/javascripts/views/content/entry_page.js index 938aa04fae..19bd9c6442 100644 --- a/assets/javascripts/views/content/entry_page.js +++ b/assets/javascripts/views/content/entry_page.js @@ -23,6 +23,7 @@ app.views.EntryPage = class EntryPage extends app.View { deactivate() { if (super.deactivate(...arguments)) { + this.hideTransientNotice(); this.empty(); this.entry = null; } @@ -250,14 +251,21 @@ app.views.EntryPage = class EntryPage extends app.View { } showTransientNotice(type) { - if (this.transientNotice) { - clearTimeout(this.transientNoticeTimer); - this.transientNotice.deactivate(); - } + this.hideTransientNotice(); this.transientNotice = new app.views.Notice(type); - this.transientNoticeTimer = setTimeout(() => { - this.transientNotice.deactivate(); - this.transientNotice = null; - }, 3000); + // Persistent notices (single doc, disabled doc) share the same bounds and + // z-index, so raise this one to keep it visible while it's shown. + this.transientNotice.addClass("_notice-transient"); + this.transientNoticeTimer = this.delay(this.hideTransientNotice, 3000); + } + + hideTransientNotice() { + if (!this.transientNotice) { + return; + } + clearTimeout(this.transientNoticeTimer); + this.transientNotice.deactivate(); + this.transientNotice = null; + this.transientNoticeTimer = null; } }; diff --git a/assets/stylesheets/components/_notice.scss b/assets/stylesheets/components/_notice.scss index 497a8989b4..9ccbb039a2 100644 --- a/assets/stylesheets/components/_notice.scss +++ b/assets/stylesheets/components/_notice.scss @@ -18,6 +18,8 @@ ~ ._container { padding-bottom: 2.5rem; } } +._notice-transient { z-index: calc(var(--noticeZ) + 1); } + ._notice-text { display: table-cell; vertical-align: middle; From 254df25eca00f5e35d8a2fd743fcb0e3ca2d1734 Mon Sep 17 00:00:00 2001 From: Simon Legner Date: Sun, 13 Sep 2026 13:24:35 +0200 Subject: [PATCH 5/5] Don't report a copy failure after leaving the page --- assets/javascripts/views/content/entry_page.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/views/content/entry_page.js b/assets/javascripts/views/content/entry_page.js index 5b6c30b7a6..2fc24d4b5d 100644 --- a/assets/javascripts/views/content/entry_page.js +++ b/assets/javascripts/views/content/entry_page.js @@ -236,9 +236,14 @@ app.views.EntryPage = class EntryPage extends app.View { this.showTransientNotice("copyFailed"); return; } - navigator.clipboard - .writeText(link.href + location.hash) - .catch(() => this.showTransientNotice("copyFailed")); + navigator.clipboard.writeText(link.href + location.hash).catch(() => { + // The rejection may arrive after the user navigated away. This view is + // reused across entries, so only report the failure while the page that + // was copied from is still the one on screen. + if (this.activated && link.isConnected) { + this.showTransientNotice("copyFailed"); + } + }); } onAltO() {