diff --git a/.gitattributes b/.gitattributes
index ad6142a..44d2276 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -23,3 +23,4 @@ composer.lock export-ignore
README.md export-ignore
.editorconfig export-ignore
docs/images/ export-ignore
+docs/update/ export-ignore
diff --git a/README.md b/README.md
index 161815e..3dcba7f 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ Click a forum, topic, post or FAQ heading to copy a link to it, ready-made as BB
## Features
- Click a forum or topic title, a post subject, or a question in the FAQ or BBCode guide, and a link to it is copied as BBCode, for example `[url=https://example.com/forum/viewtopic.php?t=42]Topic title[/url]`. Paste it into a post and it shows as a readable link.
+- Code blocks have a separate **Copy code** button beside **Select all**. It copies plain text, preserving indentation and line breaks, and provides visible and screen reader feedback. If copying is blocked, the code is selected for manual copying. The ACP link-format setting does not affect code copying.
- Also works on the title above the posting form and on the posts in the topic review below it.
- Links are full addresses built from the forum, topic or post ID, so they work anywhere they're pasted and never carry a session ID. They always point at the forum, topic or post itself, not a page of it.
- A small icon (Font Awesome `fa-clone` by default) marks each copyable heading and turns into a tick for a moment after copying. Screen readers announce "The link has been copied."
@@ -54,7 +55,7 @@ Contributions are welcome!
## Acknowledgments
- Based on [Click/tap/Enter to copy pre-formatted links](https://www.phpbb.com/community/viewtopic.php?t=2676238), style code shared on phpBB.com by Gumboots, which itself grew out of the older Post Numbers extension.
-- Code review, bug fixes, and documentation assisted by [Claude](https://www.anthropic.com/claude).
+- Code review, bug fixes, and documentation assisted by [Claude](https://www.anthropic.com/claude) and [Codex](https://openai.com/codex/).
## License
diff --git a/docs/images/screenshots/README.md b/docs/images/screenshots/README.md
index 6bedf60..4210761 100644
--- a/docs/images/screenshots/README.md
+++ b/docs/images/screenshots/README.md
@@ -20,6 +20,14 @@ Clicking a question in the FAQ copies a link to that question.
+## Code block
+
+The separate **Copy code** button copies plain code without BBCode. **Select all**
+remains available. This Playwright screenshot shows the success label on phpBB
+3.3 after an actual clipboard write and read.
+
+
+
## ACP settings
**Extensions → Copy Links → Settings**: choose plain URLs or BBCode (the default), turn the icon on or off, and choose which Font Awesome icon it is. With the icon off, only the tick shows after a copy.
diff --git a/docs/images/screenshots/copylinks-code.png b/docs/images/screenshots/copylinks-code.png
new file mode 100644
index 0000000..9931cae
Binary files /dev/null and b/docs/images/screenshots/copylinks-code.png differ
diff --git a/docs/update/2026-10-25/01-copy-code.png b/docs/update/2026-10-25/01-copy-code.png
new file mode 100644
index 0000000..e4500f3
Binary files /dev/null and b/docs/update/2026-10-25/01-copy-code.png differ
diff --git a/docs/update/2026-10-25/02-code-copied.png b/docs/update/2026-10-25/02-code-copied.png
new file mode 100644
index 0000000..9794f37
Binary files /dev/null and b/docs/update/2026-10-25/02-code-copied.png differ
diff --git a/docs/update/2026-10-25/03-copy-manually.png b/docs/update/2026-10-25/03-copy-manually.png
new file mode 100644
index 0000000..f6afc7e
Binary files /dev/null and b/docs/update/2026-10-25/03-copy-manually.png differ
diff --git a/docs/update/2026-10-25/README.md b/docs/update/2026-10-25/README.md
new file mode 100644
index 0000000..1dd4d4b
--- /dev/null
+++ b/docs/update/2026-10-25/README.md
@@ -0,0 +1,27 @@
+# Copy Links update screenshots — 10/25/2026
+
+Recent changes in the rolling `1.0.0-dev` build. The complete screenshot gallery
+remains at [docs/images/screenshots/README.md](../../images/screenshots/README.md).
+
+Captured on phpBB 3.3 with prosilver and seed-forum fixtures using
+`tests/browser/capture_update.py`, which reuses phpbb-test-board’s screenshot
+tool. Clipboard success and failure are controlled for reproducible feedback
+states. The PNGs retain their original resolution.
+
+## Copy code control
+
+A separate Copy code button appears beside Select all. Keyboard focus remains visible.
+
+
+
+## Successful copy
+
+The button shows Copied! after plain code is copied. Indentation and line breaks are preserved.
+
+
+
+## Manual-copy fallback
+
+When clipboard access is unavailable, the code is selected and the button shows Copy manually.
+
+
diff --git a/language/en/common.php b/language/en/common.php
index ca37ebd..775de10 100644
--- a/language/en/common.php
+++ b/language/en/common.php
@@ -19,6 +19,11 @@
}
$lang = array_merge($lang, [
+ 'COPYLINKS_CODE' => 'Copy code',
+ 'COPYLINKS_CODE_FAILED_LABEL' => 'Copy manually',
+ 'COPYLINKS_CODE_COPIED_LABEL' => 'Copied!',
+ 'COPYLINKS_CODE_COPIED' => 'The code has been copied.',
+ 'COPYLINKS_CODE_COPY_FAILED' => 'Automatic copying failed. The code is selected; copy it manually.',
'COPYLINKS_HINT_PLAIN' => 'Copy a link to this',
'COPYLINKS_HINT' => 'Copy a link to this as BBCode',
'COPYLINKS_COPIED' => 'The link has been copied.',
diff --git a/styles/all/template/copylinks.js b/styles/all/template/copylinks.js
index 8186de6..1f1d33c 100644
--- a/styles/all/template/copylinks.js
+++ b/styles/all/template/copylinks.js
@@ -2,7 +2,8 @@
* Copy Links extension for the phpBB Forum Software package
*
* Turns forum, topic, post and FAQ headings into "copy link" controls. A
- * click copies a BBCode link or a plain URL to the clipboard.
+ * click copies a BBCode link or a plain URL to the clipboard. Code blocks
+ * have a separate control that copies their contents as plain text.
* Links are built from IDs, so they never carry a session ID, and always point
* at the forum, topic or post itself rather than a page within it.
*
@@ -24,6 +25,13 @@
var hint = status.getAttribute('data-hint');
var copiedMessage = status.getAttribute('data-copied');
var failedMessage = status.getAttribute('data-copy-failed');
+ var codeLabel = status.getAttribute('data-code-label');
+ var codeCopiedLabel = status.getAttribute('data-code-copied-label');
+ var codeCopiedMessage = status.getAttribute('data-code-copied');
+ var codeFailedMessage = status.getAttribute('data-code-copy-failed');
+ var codeFailedLabel = status.getAttribute('data-code-failed-label');
+ var announceTimer;
+ var statusTimer;
// How long the tick and the status message stay, in milliseconds
var SHOW_COPIED_FOR = 2000;
@@ -106,49 +114,71 @@
return '[url=' + url + ']' + text.replace(/\[(\/?url)/gi, '($1') + '[/url]';
}
+ /**
+ * Announce the latest action, including repeated identical messages.
+ *
+ * @param {string} message
+ */
+ function announce(message) {
+ window.clearTimeout(announceTimer);
+ window.clearTimeout(statusTimer);
+ status.textContent = '';
+ announceTimer = window.setTimeout(function () {
+ status.textContent = message;
+ }, ANNOUNCE_DELAY);
+ statusTimer = window.setTimeout(function () {
+ status.textContent = '';
+ }, SHOW_COPIED_FOR);
+ }
+
/**
* Show the tick and announce the copy to screen readers.
*
* @param {Element} target
+ * @param {string} message
*/
- function showCopied(target) {
+ function showCopied(target, message) {
var targetIcon = target.querySelector('.copylinks-icon');
target.classList.add('copylinks-copied');
+ if (target.classList.contains('copylinks-code')) {
+ target.textContent = codeCopiedLabel;
+ }
if (targetIcon) {
targetIcon.classList.remove(icon);
targetIcon.classList.add(COPIED_ICON);
}
- // Clear first so the same message is read out again on a repeat copy
- status.textContent = '';
- window.setTimeout(function () {
- status.textContent = copiedMessage;
- }, ANNOUNCE_DELAY);
+ announce(message);
// A repeat copy restarts the timer instead of being cut short by the
// previous one
window.clearTimeout(target.copylinksTimer);
target.copylinksTimer = window.setTimeout(function () {
target.classList.remove('copylinks-copied');
+ if (target.classList.contains('copylinks-code')) {
+ target.textContent = codeLabel;
+ }
if (targetIcon) {
targetIcon.classList.remove(COPIED_ICON);
targetIcon.classList.add(icon);
}
- status.textContent = '';
}, SHOW_COPIED_FOR);
}
/**
* Copy text to the clipboard, or let the reader copy it by hand when the
- * browser does not allow it (plain http, private windows, old browsers).
+ * browser does not allow it (for example, on plain http).
*
* @param {Element} target
* @param {string} text
+ * @param {string} successMessage
+ * @param {string} failureMessage
+ * @param {Function} [fallback] Optional manual-copy action for code blocks
*/
- function copy(target, text) {
- var copyByHand = function () {
- window.prompt(failedMessage, text);
+ function copy(target, text, successMessage, failureMessage, fallback) {
+ var copyByHand = fallback || function () {
+ window.prompt(failureMessage, text);
};
if (!navigator.clipboard || !window.isSecureContext) {
@@ -157,7 +187,7 @@
}
navigator.clipboard.writeText(text).then(function () {
- showCopied(target);
+ showCopied(target, successMessage);
}, copyByHand);
}
@@ -209,6 +239,37 @@
mark(anchor, 'faq');
});
+ // Keep Select all intact and add a native button beside it. Reading the
+ // rendered code preserves indentation, line breaks and highlighted text.
+ document.querySelectorAll('.codebox').forEach(function (box) {
+ var header = box.querySelector('p');
+ var code = box.querySelector('pre > code');
+ if (!header || !code) {
+ return;
+ }
+
+ var button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'copylinks-code';
+ button.textContent = codeLabel;
+ button.addEventListener('click', function () {
+ copy(button, code.innerText, codeCopiedMessage, codeFailedMessage, function () {
+ // Select the code in place: a prompt's single-line field is not
+ // suitable for manually copying multiline code.
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.selectNodeContents(code);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ window.clearTimeout(button.copylinksTimer);
+ button.classList.remove('copylinks-copied');
+ button.textContent = codeFailedLabel;
+ announce(codeFailedMessage);
+ });
+ });
+ header.appendChild(button);
+ });
+
// One listener for every control. Clicks with a modifier key or a
// middle button keep their normal behaviour, such as opening a new tab.
document.addEventListener('click', function (event) {
@@ -229,6 +290,6 @@
window.location.hash = fragment;
}
target.focus({preventScroll: true});
- copy(target, plainUrl ? url : bbcodeFor(url, headingText(target)));
+ copy(target, plainUrl ? url : bbcodeFor(url, headingText(target)), copiedMessage, failedMessage);
});
})();
diff --git a/styles/all/template/event/overall_footer_after.html b/styles/all/template/event/overall_footer_after.html
index 76959dd..25b6843 100644
--- a/styles/all/template/event/overall_footer_after.html
+++ b/styles/all/template/event/overall_footer_after.html
@@ -1,5 +1,10 @@
{% if not S_IS_BOT %}