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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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.
- Code blocks use one **Copy code** button in place of **Select all** when JavaScript runs. With JavaScript off, the original **Select all** link remains. 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
8 changes: 4 additions & 4 deletions docs/images/screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ 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.
**Copy code** replaces **Select all** when JavaScript runs. With JavaScript off,
the original link remains. The screenshot shows the success label on phpBB 3.3,
captured with the shared screenshot tool using a controlled clipboard outcome.

<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>
<a href="copylinks-code.png"><img src="copylinks-code.png" width="700" height="73" alt="Single code-copy control showing Copied feedback"></a>

## ACP settings

Expand Down
Binary file modified 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 modified 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 modified 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 modified 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.
6 changes: 3 additions & 3 deletions docs/update/2026-10-25/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ 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 replaces Select all when JavaScript runs, leaving one control. With JavaScript off, the original Select all link remains. Keyboard focus stays 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>
<a href="01-copy-code.png"><img src="01-copy-code.png" width="640" height="67" alt="Single Copy code button with a visible keyboard focus outline"></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>
<a href="02-code-copied.png"><img src="02-code-copied.png" width="640" height="67" alt="Single code-copy control showing Copied success feedback"></a>

## Manual-copy fallback

Expand Down
9 changes: 7 additions & 2 deletions styles/all/template/copylinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
mark(anchor, 'faq');
});

// Keep Select all intact and add a native button beside it. Reading the
// Replace Select all only after the copy button is ready. Reading the
// rendered code preserves indentation, line breaks and highlighted text.
document.querySelectorAll('.codebox').forEach(function (box) {
var header = box.querySelector('p');
Expand Down Expand Up @@ -267,7 +267,12 @@
announce(codeFailedMessage);
});
});
header.appendChild(button);
var selectAll = header.querySelector('a[onclick*="selectCode("]');
if (selectAll) {
header.replaceChild(button, selectAll);
} else {
header.appendChild(button);
}
});

// One listener for every control. Clicks with a modifier key or a
Expand Down
3 changes: 1 addition & 2 deletions styles/all/theme/copylinks.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@
}
}

/* Text button beside Select all, using the codebox header's typography. */
/* Copy control using the codebox header's typography. */
.codebox .copylinks-code {
margin-inline-start: 0.75em;
padding: 0;
border: 0;
background: transparent;
Expand Down
4 changes: 2 additions & 2 deletions tests/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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.
a 320-pixel viewport, existing heading copying, one code control with JavaScript,
and the original Select all link and content with JavaScript disabled. Clipboard writes are stubbed.

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

Expand Down
11 changes: 5 additions & 6 deletions tests/browser/code-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const { chromium } = require('playwright');
const buttons = page.locator('.copylinks-code');
await buttons.first().waitFor();
assert.ok(await buttons.count() >= 2, 'fixture needs two code blocks');
assert.equal(await page.locator('.codebox p a[onclick*="selectCode("]').count(), 0, 'Select all is replaced when JavaScript runs');
assert.equal(await buttons.count(), await page.locator('.codebox pre > code').count());
const expected = await page.locator('.codebox pre > code').allInnerTexts();
const first = buttons.first();
await first.click();
Expand All @@ -43,10 +45,6 @@ const { chromium } = require('playwright');
await page.waitForFunction(() => window.copiedTexts.length === 4);
assert.equal(await page.evaluate(() => window.copiedTexts[3]), expected[1]);
await page.waitForFunction(() => Array.from(document.querySelectorAll('.copylinks-code')).every(button => button.textContent === 'Copy code'));
await page.locator('.codebox p a').first().click();
// Chromium's native selection omits the final block newline.
assert.equal(await page.evaluate(() => window.getSelection().toString()), expected[0].replace(/\n$/, ''));
assert.equal(await page.evaluate(() => window.copiedTexts.length), 4);

// Existing heading copying still uses the configured output format.
await page.locator('h2.topic-title > a').click();
Expand Down Expand Up @@ -100,10 +98,11 @@ const { chromium } = require('playwright');
const plainPage = await noJS.newPage();
await plainPage.goto(process.env.COPYLINKS_TEST_URL);
assert.equal(await plainPage.locator('.copylinks-code').count(), 0);
assert.ok(await plainPage.locator('.codebox p a').count() >= 2);
assert.ok(await plainPage.locator('.codebox p a[onclick*="selectCode("]').count() >= 2);
assert.equal(await plainPage.getByRole('link', {name: 'Select all', exact: true}).count(), expected.length);
assert.deepEqual(await plainPage.locator('.codebox pre > code').allTextContents(), expected);
await noJS.close();
console.log('PASS: exact code, multiple blocks, Enter/Space, repeat feedback, Select all, heading format, denied/missing/insecure clipboard selection fallback and recovery, focus, 320px layout, accessible name, no-JS content.');
console.log('PASS: exact code, multiple blocks, Enter/Space, repeat feedback, one code control, heading format, denied/missing/insecure clipboard selection fallback and recovery, focus, 320px layout, accessible name, no-JS Select all and content.');
} finally {
await browser.close();
}
Expand Down
Loading