mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
The copy-code-block feature existed but the reporter saw it 'work very sporadically (only a handful of items have it)' and lost after editing. Root causes: it ran ONCE in Template.editor.onRendered with a global document.querySelectorAll('.viewer > pre'), so it decorated only whichever viewers were on the page at that instant and never re-added the button after an edit re-rendered the viewer; and it copied the HTML-escaped innerHTML of the <pre>'s first child node (often the wrong content or undefined) via the deprecated execCommand. Now the decoration lives in Template.viewer.onRendered, so EVERY viewer (card description, checklist item, comment) gets a copy button and it is restored on every re-render; it is de-duplicated (skips a <pre> that already has its button) so re-renders never stack buttons; it copies the RAW code text (pre.textContent) via navigator.clipboard.writeText with an execCommand fallback; and the button title is translated (the existing 'copy-text-to-clipboard' i18n key) instead of a hard-coded English string.
Tests: tests/copyCodeBlock.test.cjs (5 checks) guards that the decoration is in Template.viewer.onRendered scoped to the viewer's <pre>, copies textContent (not innerHTML), de-dups, is translated, and uses the async Clipboard API with a fallback.
Thanks to C0rn3j and xet7 !
Fixes #5149,
68 lines
3.1 KiB
JavaScript
68 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
// Regression guard for #5149 ("Copy code block to clipboard"). The feature adds a
|
|
// copy button to each code block in the read-only card viewer. It previously ran
|
|
// once in Template.editor.onRendered with a global `document.querySelectorAll`,
|
|
// so it decorated only whichever viewers were on the page at that instant ("works
|
|
// sporadically"), lost the button after editing, and copied the HTML-escaped
|
|
// innerHTML of childNodes[0] (wrong/undefined content). It now decorates in
|
|
// Template.viewer.onRendered — per viewer, on every re-render, de-duplicated,
|
|
// copying the raw text. This is a source guard (the code is Blaze/DOM-coupled).
|
|
// Run: node tests/copyCodeBlock.test.cjs
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const src = fs.readFileSync(
|
|
path.join(__dirname, '..', 'client', 'components', 'main', 'editor.js'),
|
|
'utf8',
|
|
);
|
|
|
|
let passed = 0;
|
|
function check(name, fn) { fn(); passed += 1; console.log(' ok -', name); }
|
|
|
|
check('#5149: the copy-code decoration runs in Template.viewer.onRendered (per viewer)', () => {
|
|
assert.ok(/Template\.viewer\.onRendered\(/.test(src),
|
|
'must decorate in Template.viewer.onRendered, not a one-time global query');
|
|
const start = src.indexOf('Template.viewer.onRendered(');
|
|
const body = src.slice(start, start + 900);
|
|
assert.ok(/this\.\$\('pre'\)/.test(body),
|
|
"must scope to the viewer's own <pre> blocks (this.$('pre'))");
|
|
});
|
|
|
|
check('#5149: it copies the RAW text (pre.textContent), not innerHTML', () => {
|
|
assert.ok(/copyCodeBlockText\(pre\.textContent\)/.test(src),
|
|
'must copy pre.textContent');
|
|
assert.ok(!/textArea\.value = target\.innerHTML/.test(src),
|
|
'the old innerHTML-of-childNodes[0] copy must be gone');
|
|
assert.ok(!/querySelectorAll\("\.viewer > pre"\)/.test(src),
|
|
'the old global one-time query must be gone');
|
|
});
|
|
|
|
check('#5149: buttons are de-duplicated (no stacking on re-render)', () => {
|
|
const start = src.indexOf('Template.viewer.onRendered(');
|
|
const body = src.slice(start, start + 900);
|
|
assert.ok(/js-copy-code/.test(body), 'the button carries a js-copy-code marker class');
|
|
assert.ok(/previousElementSibling[\s\S]*js-copy-code[\s\S]*return/.test(body),
|
|
'must skip a <pre> that already has its copy button');
|
|
});
|
|
|
|
check('#5149: the button title is translated (not a hard-coded English string)', () => {
|
|
const start = src.indexOf('Template.viewer.onRendered(');
|
|
const body = src.slice(start, start + 900);
|
|
assert.ok(/TAPi18n\.__\('copy-text-to-clipboard'\)/.test(body),
|
|
"must use the i18n key 'copy-text-to-clipboard'");
|
|
const en = JSON.parse(fs.readFileSync(
|
|
path.join(__dirname, '..', 'imports', 'i18n', 'data', 'en.i18n.json'), 'utf8'));
|
|
assert.ok(en['copy-text-to-clipboard'], 'the i18n key must exist in en.i18n.json');
|
|
});
|
|
|
|
check('#5149: uses the async Clipboard API with an execCommand fallback', () => {
|
|
assert.ok(/navigator\.clipboard && navigator\.clipboard\.writeText/.test(src),
|
|
'must prefer navigator.clipboard.writeText');
|
|
assert.ok(/fallbackCopyText/.test(src), 'must have an execCommand fallback');
|
|
});
|
|
|
|
console.log(`\ncopyCodeBlock: ${passed} checks passed`);
|