fix(runtime): use jQuery, not bare $, in the lazily loaded clipboard lib

WordPress runs jQuery in noConflict mode, so bare $ is undefined. The
clipboard module called $(this) on every click and $.isFunction on the
copy result, so any click on a [data-clipboard-text] button threw
"$ is not a function" and copied nothing.

This was latent: the only prior emitter was the paid card, and the click
path only ran when a user actually clicked it. Exposing the copy button on
the download box made it reachable on every resource, surfacing the error.

Bind through jQuery explicitly, guard notyf so a missing toast lib cannot
also throw, and drop the _win.bd dependency (that payload only gains .bd
once main.js runs). Contract-lock the module under simulated noConflict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 21:13:24 +08:00
co-authored by Claude Sonnet 4.6
parent bd3d50c6c6
commit 63b5edd901
3 changed files with 104 additions and 5 deletions
+4 -4
View File
@@ -38,14 +38,14 @@ function copyText(text, success, error, _this) {
}
}
_win.bd.on('click', "[data-clipboard-text]", 'clipboard', function (e) {
var _this = $(this);
window.jQuery('body').on('click', "[data-clipboard-text]", 'clipboard', function (e) {
var _this = jQuery(this);
var text = _this.attr('data-clipboard-text');
var tag = _this.attr('data-clipboard-tag') || '内容';
copyText(text, function () {
notyf(tag + "已复制")
typeof notyf === 'function' && notyf(tag + "已复制")
}, function () {
notyf(tag + "复制失败,请手动复制", 'danger')
typeof notyf === 'function' && notyf(tag + "复制失败,请手动复制", 'danger')
}, this);
})
+1 -1
View File
@@ -1 +1 @@
function copyText(t,e,n,a){function c(t,e,n){if(t.createTextRange){var a=t.createTextRange();a.collapse(!0),a.moveStart("character",e),a.moveEnd("character",n-e),a.select()}else t.setSelectionRange(e,n),t.select()}var o=t.toString(),i=document.querySelector("#copy-input");i||(i=document.createElement("input"),i.id="copy-input",i.readOnly="readOnly",i.style.position="fixed",i.style.left="-2000px",i.style.zIndex="-1000",a.parentNode.appendChild(i)),i.value=o,c(i,0,o.length),document.execCommand("copy")?$.isFunction(e)&&e():$.isFunction(n)&&n(),i.blur()}_win.bd.on("click","[data-clipboard-text]","clipboard",function(t){var e=$(this),n=e.attr("data-clipboard-text"),a=e.attr("data-clipboard-tag")||"内容";copyText(n,function(){notyf(a+"已复制")},function(){notyf(a+"复制失败,请手动复制","danger")},this)});
function copyText(t,e,n,a){function c(t,e,n){if(t.createTextRange){var a=t.createTextRange();a.collapse(!0),a.moveStart("character",e),a.moveEnd("character",n-e),a.select()}else{t.setSelectionRange(e,n),t.select()}}var o=t.toString(),i=document.querySelector("#copy-input");if(!i){i=document.createElement("input");i.id="copy-input";i.readOnly="readOnly";i.style.position="fixed";i.style.left="-2000px";i.style.zIndex="-1000";a.parentNode.appendChild(i)}i.value=o;c(i,0,o.length);if(document.execCommand("copy")){if(typeof e==="function")e()}else{if(typeof n==="function")n()}i.blur()}window.jQuery("body").on("click","[data-clipboard-text]","clipboard",function(t){var e=jQuery(this),n=e.attr("data-clipboard-text"),a=e.attr("data-clipboard-tag")||"内容";copyText(n,function(){if(typeof notyf==="function")notyf(a+"已复制")},function(){if(typeof notyf==="function")notyf(a+"复制失败,请手动复制","danger")},this)});
@@ -0,0 +1,99 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
// Strip string literals so a "$" inside a message is not mistaken for a call.
const stripStrings = (src) =>
src.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
// A jQuery stand-in exposing only what clipboard.min.js touches. Bare $ is
// deliberately NOT provided, matching WordPress's noConflict mode.
function makeSandbox() {
const handlers = [];
const created = [];
const toasts = [];
const body = {
on(evt, childSel, ns, fn) {
handlers.push({ evt, childSel, ns, fn });
return this;
},
};
const documentStub = {
querySelector: (sel) => (sel === '#copy-input' ? created[created.length - 1] || null : null),
createElement: () => {
const el = {
style: {}, value: '', id: '', readOnly: '',
setSelectionRange() {}, select() {}, blur() {},
};
created.push(el);
return el;
},
execCommand: () => true,
};
const jq = (sel) => {
if (sel === 'body') return body;
if (sel && sel.attrs) return { attr: (n) => sel.attrs[n] };
return { on() { return this; } };
};
jq.__isElement = true;
return { sandbox: { window: {}, document: documentStub, jQuery: jq, notyf: (m, t) => toasts.push({ m, t }) }, handlers, created, toasts, body };
}
test('the lazily loaded clipboard lib survives WordPress noConflict', () => {
const src = read('assets/js/runtime/libs/clipboard.min.js');
// No bare $ calls or $.<method> usage anywhere in the module.
const bare = stripStrings(src);
assert.doesNotMatch(bare, /(^|[^.\w$])\$\(/, 'clipboard.min.js uses a bare $(');
assert.doesNotMatch(bare, /\$\.\w+/, 'clipboard.min.js uses a bare $.<method>');
// It must bind through jQuery explicitly on the element that always exists.
assert.match(src, /jQuery\("body"\)\.on\("click",\s*"\[data-clipboard-text\]"/);
const { sandbox, handlers, created, toasts } = makeSandbox();
sandbox.window.jQuery = sandbox.jQuery;
// Evaluating with no global $ must not throw.
assert.doesNotThrow(() => {
new Function('window', 'document', 'jQuery', 'notyf', '"use strict";\n' + src)(
sandbox.window, sandbox.document, sandbox.jQuery, sandbox.notyf
);
});
assert.equal(handlers.length, 1);
assert.equal(handlers[0].evt, 'click');
assert.equal(handlers[0].childSel, '[data-clipboard-text]');
// Clicking must copy the copy content and toast the copy name.
const el = {
attrs: { 'data-clipboard-text': 'a1b2', 'data-clipboard-tag': '提取码' },
parentNode: { appendChild() {} },
};
assert.doesNotThrow(() => handlers[0].fn.call(el, {}));
assert.equal(created[created.length - 1].value, 'a1b2');
assert.deepEqual(toasts, [{ m: '提取码已复制', t: undefined }]);
});
test('the readable clipboard source agrees with the loaded bundle', () => {
const src = read('assets/js/runtime/libs/clipboard.js');
const bare = stripStrings(src);
assert.doesNotMatch(bare, /(^|[^.\w$])\$\(/, 'clipboard.js uses a bare $(');
assert.match(src, /jQuery\('body'\)\.on\('click'/);
assert.match(src, /typeof notyf === 'function'/);
});
test('bootstrap is unaffected because it receives jQuery as an IIFE argument', () => {
const src = read('assets/js/runtime/libs/bootstrap.min.js');
assert.match(src, /!function\(\w\)\{"use strict"/);
assert.match(src, /requires jQuery/);
});