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. Tick after copying an FAQ 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. + +Copied feedback beside Select all in a code block + ## 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. + +Copy code button with a visible keyboard focus outline beside Select all + +## Successful copy + +The button shows Copied! after plain code is copied. Indentation and line breaks are preserved. + +Copied success feedback beside Select all + +## Manual-copy fallback + +When clipboard access is unavailable, the code is selected and the button shows Copy manually. + +Selected code and Copy manually feedback after automatic copying fails 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 %}