From d74d6093f318527addf73cb74c526926c901f339 Mon Sep 17 00:00:00 2001 From: Ehsan-hash Date: Thu, 15 Oct 2026 00:04:42 -0700 Subject: [PATCH] Preserve ideographic spaces when copying text --- src/clipboard.ts | 4 +-- test/test.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/clipboard.ts b/src/clipboard.ts index 8e1bb0f..fd99971 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -11,9 +11,7 @@ function createNode(text: string): Element { // Normalize characters that render as a space (or as nothing) so that copied // text matches what the user sees. function normalizeText(text: string): string { - return text - .replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, ' ') - .replace(/[\u200B-\u200D\u2060\uFEFF\u180E]/g, '') + return text.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F]/g, ' ').replace(/[\u200B-\u200D\u2060\uFEFF\u180E]/g, '') } function copySelection(node: Element): Promise { diff --git a/test/test.js b/test/test.js index ab1c7da..1f9cb1e 100644 --- a/test/test.js +++ b/test/test.js @@ -179,7 +179,7 @@ describe('clipboard-copy element', function () { assert.notInclude(text, '\u00A0') }) - it('normalizes Unicode space-separator characters to regular spaces', async function () { + it('normalizes Unicode space-separator characters except ideographic spaces', async function () { const spaceChars = [ '\u00A0', '\u1680', @@ -196,7 +196,6 @@ describe('clipboard-copy element', function () { '\u200A', '\u202F', '\u205F', - '\u3000', ] for (const char of spaceChars) { @@ -215,6 +214,29 @@ describe('clipboard-copy element', function () { } }) + it('preserves ideographic spaces in target element text', async function () { + const target = document.createElement('div') + target.textContent = 'product\u3000name' + target.id = 'copy-target' + document.body.append(target) + + const button = document.querySelector('clipboard-copy') + button.click() + + const text = await whenCopied + assert.equal(text, 'product\u3000name') + }) + + it('preserves ideographic spaces from the value attribute', async function () { + const button = document.querySelector('clipboard-copy') + button.setAttribute('value', 'product\u3000name') + + button.click() + + const text = await whenCopied + assert.equal(text, 'product\u3000name') + }) + it('strips zero-width and invisible format characters', async function () { const zeroWidthChars = ['\u200B', '\u200C', '\u200D', '\u2060', '\uFEFF', '\u180E'] @@ -234,6 +256,45 @@ describe('clipboard-copy element', function () { } }) + it('preserves ideographic spaces through the legacy fallback', async function () { + const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'clipboard') + const execCommandDescriptor = Object.getOwnPropertyDescriptor(document, 'execCommand') + assert(descriptor && descriptor.configurable) + + delete navigator.clipboard + delete Navigator.prototype.clipboard + assert.equal('clipboard' in navigator, false) + + let selectedText + Object.defineProperty(document, 'execCommand', { + configurable: true, + value(command) { + if (command === 'copy') selectedText = getSelection().toString() + return true + }, + }) + + try { + const target = document.createElement('div') + target.textContent = 'product\u3000name' + target.id = 'copy-target' + document.body.append(target) + + const button = document.querySelector('clipboard-copy') + button.click() + await whenCopied + } finally { + if (execCommandDescriptor) { + Object.defineProperty(document, 'execCommand', execCommandDescriptor) + } else { + delete document.execCommand + } + Object.defineProperty(Navigator.prototype, 'clipboard', descriptor) + } + + assert.equal(selectedText, 'product\u3000name') + }) + it('does not copy when disabled', async function () { const target = document.createElement('div') target.innerHTML = 'Hello world!'