fix: prevent duplicate PIN copy handlers from multiple modules
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled

- Guard with window._mokoPinBound and data-bound to prevent
  multi-module pages from attaching N click handlers per badge
- Extract bindCopy() for reuse after request-then-copy flow
- Ensure title="Click to copy" tooltip on all PIN elements
This commit is contained in:
2026-06-25 11:48:03 -05:00
parent 4276f95fb6
commit 9de56e605a
@@ -181,8 +181,14 @@ class SupportPinHelper
{
return <<<'JS'
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuiteclient-pin-copy').forEach(function(el) {
(function() {
if (window._mokoPinBound) return;
window._mokoPinBound = true;
function bindCopy(el) {
if (el.dataset.bound) return;
el.dataset.bound = '1';
if (!el.title) el.title = 'Click to copy';
el.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
@@ -194,62 +200,60 @@ document.addEventListener('DOMContentLoaded', function() {
var orig = textEl.textContent;
textEl.textContent = 'Copied!';
setTimeout(function() { textEl.textContent = orig; }, 30000);
} else {
Joomla.renderMessages({message: ['PIN copied: ' + pin]});
}
});
}
});
});
document.querySelectorAll('.mokosuiteclient-pin-request').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
if (this.dataset.busy) return;
this.dataset.busy = '1';
var btn = this;
var textEl = btn.querySelector('.mokosuiteclient-pin-text');
var origHtml = btn.innerHTML;
if (textEl) { textEl.textContent = '...'; } else { btn.textContent = '...'; }
var fd = new FormData();
fd.append(btn.dataset.token, '1');
fetch(btn.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success && data.pin) {
btn.classList.remove('mokosuiteclient-pin-request');
btn.classList.add('mokosuiteclient-pin-copy');
btn.dataset.pin = data.pin;
btn.title = 'Click to copy';
if (textEl) {
textEl.textContent = data.pin;
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuiteclient-pin-copy').forEach(bindCopy);
document.querySelectorAll('.mokosuiteclient-pin-request').forEach(function(el) {
if (el.dataset.bound) return;
el.dataset.bound = '1';
if (!el.title) el.title = 'Request a support PIN';
el.addEventListener('click', function(e) {
e.preventDefault();
if (this.dataset.busy) return;
this.dataset.busy = '1';
var btn = this;
var textEl = btn.querySelector('.mokosuiteclient-pin-text');
var origHtml = btn.innerHTML;
if (textEl) { textEl.textContent = '...'; } else { btn.textContent = '...'; }
var fd = new FormData();
fd.append(btn.dataset.token, '1');
fetch(btn.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success && data.pin) {
btn.classList.remove('mokosuiteclient-pin-request');
btn.classList.add('mokosuiteclient-pin-copy');
btn.dataset.pin = data.pin;
btn.title = 'Click to copy';
if (textEl) {
textEl.textContent = data.pin;
} else {
btn.className = 'badge bg-dark mokosuiteclient-pin-copy';
btn.style = 'font-family:monospace;letter-spacing:0.08em;cursor:pointer;';
btn.innerHTML = '<span class="icon-key small me-1" aria-hidden="true"></span><span class="mokosuiteclient-pin-text">' + data.pin + '</span>';
}
btn.dataset.bound = '';
bindCopy(btn);
} else {
btn.className = 'badge bg-dark mokosuiteclient-pin-copy';
btn.style = 'font-family:monospace;letter-spacing:0.08em;cursor:pointer;';
btn.innerHTML = '<span class="icon-key small me-1" aria-hidden="true"></span><span class="mokosuiteclient-pin-text">' + data.pin + '</span>';
Joomla.renderMessages({error: [data.message || 'Failed to generate PIN']});
btn.innerHTML = origHtml;
}
btn.addEventListener('click', function(ev) {
ev.preventDefault();
ev.stopPropagation();
navigator.clipboard.writeText(data.pin).then(function() {
var t = btn.querySelector('.mokosuiteclient-pin-text');
if (t) { var o = t.textContent; t.textContent = 'Copied!'; setTimeout(function() { t.textContent = o; }, 30000); }
else { Joomla.renderMessages({message: ['PIN copied: ' + data.pin]}); }
});
});
} else {
Joomla.renderMessages({error: [data.message || 'Failed to generate PIN']});
delete btn.dataset.busy;
})
.catch(function() {
Joomla.renderMessages({error: ['Network error']});
btn.innerHTML = origHtml;
}
delete btn.dataset.busy;
})
.catch(function() {
Joomla.renderMessages({error: ['Network error']});
btn.innerHTML = origHtml;
delete btn.dataset.busy;
});
delete btn.dataset.busy;
});
});
});
});
});
})();
</script>
JS;
}