Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ composer.lock export-ignore
README.md export-ignore
.editorconfig export-ignore
docs/images/ export-ignore
docs/update/ export-ignore
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions docs/images/screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ Clicking a question in the FAQ copies a link to that question.

<a href="copylinks-faq.png"><img src="copylinks-faq.png" width="700" alt="Tick after copying an FAQ question"></a>

## 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.

<a href="copylinks-code.png"><img src="copylinks-code.png" width="700" height="85" alt="Copied feedback beside Select all in a code block"></a>

## 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.
Expand Down
Binary file added docs/images/screenshots/copylinks-code.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/update/2026-10-25/01-copy-code.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/update/2026-10-25/02-code-copied.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/update/2026-10-25/03-copy-manually.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions docs/update/2026-10-25/README.md
Original file line number Diff line number Diff line change
@@ -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.

<a href="01-copy-code.png"><img src="01-copy-code.png" width="640" height="67" alt="Copy code button with a visible keyboard focus outline beside Select all"></a>

## Successful copy

The button shows Copied! after plain code is copied. Indentation and line breaks are preserved.

<a href="02-code-copied.png"><img src="02-code-copied.png" width="640" height="67" alt="Copied success feedback beside Select all"></a>

## Manual-copy fallback

When clipboard access is unavailable, the code is selected and the button shows Copy manually.

<a href="03-copy-manually.png"><img src="03-copy-manually.png" width="640" height="67" alt="Selected code and Copy manually feedback after automatic copying fails"></a>
5 changes: 5 additions & 0 deletions language/en/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
89 changes: 75 additions & 14 deletions styles/all/template/copylinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -157,7 +187,7 @@
}

navigator.clipboard.writeText(text).then(function () {
showCopied(target);
showCopied(target, successMessage);
}, copyByHand);
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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);
});
})();
5 changes: 5 additions & 0 deletions styles/all/template/event/overall_footer_after.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{% if not S_IS_BOT %}
<div id="copylinks-status" class="copylinks-sr-only" role="status" aria-live="polite"
data-code-label="{{ lang('COPYLINKS_CODE')|e('html_attr') }}"
data-code-failed-label="{{ lang('COPYLINKS_CODE_FAILED_LABEL')|e('html_attr') }}"
data-code-copied-label="{{ lang('COPYLINKS_CODE_COPIED_LABEL')|e('html_attr') }}"
data-code-copied="{{ lang('COPYLINKS_CODE_COPIED')|e('html_attr') }}"
data-code-copy-failed="{{ lang('COPYLINKS_CODE_COPY_FAILED')|e('html_attr') }}"
data-plain-url="{% if S_COPYLINKS_PLAIN_URL %}1{% else %}0{% endif %}"
data-board-url="{{ U_COPYLINKS_BOARD|e('html_attr') }}"
data-php-ext="{{ COPYLINKS_PHP_EXT|e('html_attr') }}"
Expand Down
15 changes: 14 additions & 1 deletion styles/all/theme/copylinks.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
}

/* Make keyboard focus clearly visible, whatever the style does for links */
.copylinks-target:focus-visible {
.copylinks-target:focus-visible,
.copylinks-code:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
Expand Down Expand Up @@ -53,3 +54,15 @@
transition: none;
}
}

/* Text button beside Select all, using the codebox header's typography. */
.codebox .copylinks-code {
margin-inline-start: 0.75em;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
text-decoration: underline;
cursor: copy;
}
38 changes: 38 additions & 0 deletions tests/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Browser checks

`code-copy.js` checks Copy code against a running local phpBB board with the
extension enabled. Set `COPYLINKS_TEST_URL` to a topic containing at least two
code blocks, including multiline code with indentation, blank lines and Unicode.
The script checks exact clipboard contents, multiple blocks, repeated copying,
Enter and Space, visible feedback, the status region, manual-copy selection after denied, missing or insecure clipboard access and recovery, focus,
a 320-pixel viewport, existing heading copying and Select all, and content
availability with JavaScript disabled. Clipboard writes are stubbed.

Run with Node.js and Playwright available in the environment:

```bash
COPYLINKS_TEST_URL='http://localhost:18127/viewtopic.php?t=1' \
node tests/browser/code-copy.js
```

Optionally set `COPYLINKS_SCREENSHOT` to save the first code block at the narrow
viewport. Repeat on phpBB 3.3 and 4.0 and with each ACP link-format setting.
These checks do not test spoken screen reader output.

## Dated update screenshots

`capture_update.py` reuses `phpbb-release-tools/phpbb-test-board/screenshots.py`
to capture the code-copy button, success label and manual-copy selection on a
local seeded topic. It leaves board data unchanged and controls clipboard
outcomes for reproducible screenshots. Run with the shared tool's Python venv:

```bash
../phpbb-release-tools/phpbb-test-board/.venv/bin/python \
tests/browser/capture_update.py \
--url 'http://localhost:18127/viewtopic.php?t=1' \
--out docs/update/2026-10-25
```

Each dated output directory also needs a README with captions and linked,
proportionally sized previews. Link release-update posts to that dated gallery;
the complete README screenshot gallery remains separate.
Loading
Loading