bud panels DRY refactor: extract shared skeleton into bud-btn.js — the three #id_bud_btn partials (_bud_panel post-share, _bud_invite_panel gatekeeper, _bud_add_panel My Buds) duplicated ~70% of their JS (csrf/open/close, btn click, Escape, click-outside w. optional suggestions excl., Enter handler, OK POST + JSON routing); collapse into bindBudBtn({submitUrl, autocompleteUrl?, onSuccess(data)}) so each panel keeps only its markup + an onSuccess callback (post-share: _appendLine + _appendRecipientChip + Brief.showBanner; gatekeeper: Brief.showBanner only; my-buds: _appendBudEntry); autocomplete binding folded into bud-btn.js (drives bindBudAutocomplete when autocompleteUrl is passed) so callers stop repeating the bindBudAutocomplete(...) boilerplate; dead data-share-url/data-invite-url/data-add-url attrs dropped since bindBudBtn takes the URL directly; behavior-preserving — all 21 existing FTs (test_my_buds + test_bud_btn) + 49 ITs (test_buds + test_share_post + test_brief) green

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-12 15:52:09 -04:00
parent 7015ddd534
commit 264ed5968e
4 changed files with 164 additions and 305 deletions

View File

@@ -0,0 +1,112 @@
// Shared skeleton for the three #id_bud_btn slide-out panels:
// • _bud_panel.html — post-share (POSTs to billboard:share_post)
// • _bud_invite_panel.html — gatekeeper invite (POSTs to epic:invite_gamer)
// • _bud_add_panel.html — My Buds add (POSTs to billboard:add_bud)
//
// Owns: csrf cookie read, open/close + .bud-open html-class, button click,
// Escape, click-outside, Enter-in-input, OK POST + JSON routing.
//
// Each caller drives it with `bindBudBtn({submitUrl, autocompleteUrl?, onSuccess})`.
// `onSuccess(data)` does the panel-specific DOM updates (line append, chip
// append, bud-entry append, Brief banner — whatever the response carries).
// _close({clear: true}) fires automatically on a successful response.
//
// `autocompleteUrl` enables bud-autocomplete on the input (post-share +
// gatekeeper panels) by binding bud-autocomplete.js to #id_bud_suggestions.
// My Buds omits it — the autocomplete pool is request.user.buds, which is
// the set you can't usefully re-add.
(function () {
'use strict';
window.bindBudBtn = function (opts) {
opts = opts || {};
var btn = document.getElementById('id_bud_btn');
var panel = document.getElementById('id_bud_panel');
var input = document.getElementById('id_recipient');
var ok = document.getElementById('id_bud_ok');
var html = document.documentElement;
if (!btn || !panel || !input || !ok) return;
var suggestions = opts.autocompleteUrl
? document.getElementById('id_bud_suggestions')
: null;
function _csrf() {
var m = document.cookie.match(/csrftoken=([^;]+)/);
return m ? m[1] : '';
}
function _open() {
html.classList.add('bud-open');
btn.classList.add('active');
setTimeout(function () { input.focus(); }, 60);
}
function _close(o) {
o = o || {};
html.classList.remove('bud-open');
btn.classList.remove('active');
if (o.clear !== false) input.value = '';
}
btn.addEventListener('click', function () {
if (html.classList.contains('bud-open')) _close();
else _open();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && html.classList.contains('bud-open')) _close();
});
document.addEventListener('click', function (e) {
if (!html.classList.contains('bud-open')) return;
if (panel.contains(e.target)) return;
if (e.target === btn || btn.contains(e.target)) return;
// Suggestions live outside the panel (panel has overflow:hidden
// for its scaleX slide); a click inside them must NOT close+clear.
if (suggestions && suggestions.contains(e.target)) return;
_close();
});
ok.addEventListener('click', function () {
var recipient = input.value.trim();
if (!recipient) return;
var fd = new FormData();
fd.set('recipient', recipient);
fetch(opts.submitUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'X-CSRFToken': _csrf(),
},
body: fd,
})
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
.then(function (data) {
if (typeof opts.onSuccess === 'function') opts.onSuccess(data);
_close({ clear: true });
})
.catch(function () {
// Privacy-safe response shape — even an unregistered/self
// recipient is a 200. Network/5xx land here; just close.
});
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
ok.click();
}
});
if (opts.autocompleteUrl && window.bindBudAutocomplete && suggestions) {
window.bindBudAutocomplete(input, suggestions, {
searchUrl: opts.autocompleteUrl,
});
}
};
}());