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:
112
src/apps/billboard/static/apps/billboard/bud-btn.js
Normal file
112
src/apps/billboard/static/apps/billboard/bud-btn.js
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
}());
|
||||
@@ -1,15 +1,21 @@
|
||||
{% load static %}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
{# _bud_add_panel.html — bottom-left handshake btn + slide-out add- #}
|
||||
{# bud field. Mirrors _bud_panel.html (post-share) but POSTs to #}
|
||||
{# add_bud and appends to #id_buds_list instead of #id_post_table. #}
|
||||
{# Included by my_buds.html only. #}
|
||||
{# _bud_add_panel.html — bottom-left handshake btn + slide-out add-bud #}
|
||||
{# field. Skeleton (open/close/POST) owned by bud-btn.js; this partial #}
|
||||
{# wires the add-bud success callback: {bud} → append .bud-entry to #}
|
||||
{# #id_buds_list. #}
|
||||
{# Included by my_buds.html only. #}
|
||||
{# #}
|
||||
{# No autocomplete — the bud-autocomplete pool is request.user.buds, #}
|
||||
{# which is precisely the set you can't usefully re-add. Post-share + #}
|
||||
{# gatekeeper-invite panels DO bind autocomplete. #}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
|
||||
<button id="id_bud_btn" type="button" aria-label="Add a bud">
|
||||
<i class="fa-solid fa-handshake"></i>
|
||||
</button>
|
||||
|
||||
<div id="id_bud_panel" data-add-url="{% url 'billboard:add_bud' %}">
|
||||
<div id="id_bud_panel">
|
||||
<input id="id_recipient"
|
||||
name="recipient"
|
||||
type="text"
|
||||
@@ -18,59 +24,11 @@
|
||||
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
|
||||
</div>
|
||||
|
||||
{# No autocomplete on this panel — the bud-autocomplete pool is the #}
|
||||
{# user's existing buds, which is precisely the set you can't usefully #}
|
||||
{# re-add. Post-share + gatekeeper-invite panels keep the autocomplete #}
|
||||
{# binding (re-sharing with an existing bud is a real flow). #}
|
||||
|
||||
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
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;
|
||||
|
||||
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(opts) {
|
||||
opts = opts || {};
|
||||
html.classList.remove('bud-open');
|
||||
btn.classList.remove('active');
|
||||
if (opts.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;
|
||||
_close();
|
||||
});
|
||||
|
||||
function _appendBudEntry(bud) {
|
||||
var list = document.getElementById('id_buds_list');
|
||||
if (!list || !bud) return;
|
||||
@@ -93,38 +51,11 @@
|
||||
else list.appendChild(li);
|
||||
}
|
||||
|
||||
ok.addEventListener('click', function () {
|
||||
var email = input.value.trim();
|
||||
if (!email) return;
|
||||
|
||||
var fd = new FormData();
|
||||
fd.set('recipient', email);
|
||||
|
||||
fetch(panel.dataset.addUrl, {
|
||||
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 (data.bud) _appendBudEntry(data.bud);
|
||||
_close({ clear: true });
|
||||
})
|
||||
.catch(function () {
|
||||
// Privacy-safe response shape — even an unregistered email is
|
||||
// a 200 w. {bud: null}. Network/5xx land here; just close.
|
||||
});
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
ok.click();
|
||||
}
|
||||
bindBudBtn({
|
||||
submitUrl: '{% url "billboard:add_bud" %}',
|
||||
onSuccess: function (data) {
|
||||
if (data.bud) _appendBudEntry(data.bud);
|
||||
},
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
{% load static %}
|
||||
{% load lyric_extras %}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
{# _bud_invite_panel.html — bud btn + slide-out for the gatekeeper game- #}
|
||||
{# invite flow. Replaces the legacy `<form action="invite_gamer">` #}
|
||||
{# inline panel inside _gatekeeper.html. #}
|
||||
{# #}
|
||||
{# Differences from the post-share variant (_bud_panel.html): #}
|
||||
{# • POSTs to epic:invite_gamer instead of billboard:share_post. #}
|
||||
{# • Server returns {brief, recipient_display} — no line_text (no Post #}
|
||||
{# to append a Line to). JS just shows the Brief banner. #}
|
||||
{# invite flow. Skeleton (open/close/POST + autocomplete) owned by #}
|
||||
{# bud-btn.js; this partial wires the invite success callback: server #}
|
||||
{# returns {brief, recipient_display} — no line_text (no Post to append a #}
|
||||
{# Line to). JS just shows the Brief banner. #}
|
||||
{# #}
|
||||
{# Caller must pass `room` in context. #}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
@@ -17,9 +13,7 @@
|
||||
<i class="fa-solid fa-handshake"></i>
|
||||
</button>
|
||||
|
||||
<div id="id_bud_panel"
|
||||
data-invite-url="{% url 'epic:invite_gamer' room.id %}"
|
||||
data-sharer-name="{% if request.user.is_authenticated %}{{ request.user|display_name }}{% endif %}">
|
||||
<div id="id_bud_panel">
|
||||
<input id="id_recipient"
|
||||
name="recipient"
|
||||
type="text"
|
||||
@@ -33,98 +27,13 @@
|
||||
<div id="id_bud_suggestions" class="bud-suggestions" hidden></div>
|
||||
|
||||
<script src="{% static 'apps/billboard/bud-autocomplete.js' %}"></script>
|
||||
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
|
||||
<script>
|
||||
bindBudAutocomplete(
|
||||
document.getElementById('id_recipient'),
|
||||
document.getElementById('id_bud_suggestions'),
|
||||
{ searchUrl: '{% url "billboard:search_buds" %}' }
|
||||
);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
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;
|
||||
|
||||
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(opts) {
|
||||
opts = opts || {};
|
||||
html.classList.remove('bud-open');
|
||||
btn.classList.remove('active');
|
||||
if (opts.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; clicking inside them must
|
||||
// NOT close+clear the panel.
|
||||
var sg = document.getElementById('id_bud_suggestions');
|
||||
if (sg && sg.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(panel.dataset.inviteUrl, {
|
||||
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 (window.Brief && data.brief) Brief.showBanner(data.brief);
|
||||
_close({ clear: true });
|
||||
})
|
||||
.catch(function () {
|
||||
// Privacy-safe: even unregistered/self resolves to 200
|
||||
// {brief: null}; only network/5xx land here. Just close.
|
||||
});
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
ok.click();
|
||||
}
|
||||
});
|
||||
}());
|
||||
bindBudBtn({
|
||||
submitUrl: '{% url "epic:invite_gamer" room.id %}',
|
||||
autocompleteUrl: '{% url "billboard:search_buds" %}',
|
||||
onSuccess: function (data) {
|
||||
if (window.Brief && data.brief) Brief.showBanner(data.brief);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
{% load lyric_extras %}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
{# _bud_panel.html — bottom-left handshake btn + slide-out recipient #}
|
||||
{# field for the share-post async flow. Mirror of #id_kit_btn (bottom- #}
|
||||
{# right). Included by post.html only. #}
|
||||
{# field for the share-post async flow. Skeleton (open/close/POST + auto- #}
|
||||
{# complete) owned by bud-btn.js; this partial wires the share-post #}
|
||||
{# success callback: appends a Line to #id_post_table, fires Brief.show- #}
|
||||
{# Banner, and updates the .post-header shared-with prose. #}
|
||||
{# Included by post.html only. #}
|
||||
{# #}
|
||||
{# Spec lives in functional_tests/test_bud_btn.py — write it red-first. #}
|
||||
{# Run: #}
|
||||
{# python src/manage.py test functional_tests.test_bud_btn #}
|
||||
{# Spec lives in functional_tests/test_bud_btn.py. #}
|
||||
{# ─────────────────────────────────────────────────────────────────────── #}
|
||||
|
||||
<button id="id_bud_btn" type="button" aria-label="Share with a bud">
|
||||
@@ -15,7 +16,6 @@
|
||||
</button>
|
||||
|
||||
<div id="id_bud_panel"
|
||||
data-share-url="{% url 'billboard:share_post' post.id %}"
|
||||
data-sharer-name="{% if request.user.is_authenticated %}{{ request.user|display_name }}{% endif %}">
|
||||
<input id="id_recipient"
|
||||
name="recipient"
|
||||
@@ -30,76 +30,16 @@
|
||||
<div id="id_bud_suggestions" class="bud-suggestions" hidden></div>
|
||||
|
||||
<script src="{% static 'apps/billboard/bud-autocomplete.js' %}"></script>
|
||||
<script>
|
||||
bindBudAutocomplete(
|
||||
document.getElementById('id_recipient'),
|
||||
document.getElementById('id_bud_suggestions'),
|
||||
{ searchUrl: '{% url "billboard:search_buds" %}' }
|
||||
);
|
||||
</script>
|
||||
|
||||
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
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;
|
||||
|
||||
function _csrf() {
|
||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
return m ? m[1] : '';
|
||||
}
|
||||
|
||||
function _open() {
|
||||
html.classList.add('bud-open');
|
||||
btn.classList.add('active');
|
||||
// small delay before focus so the slide-out animation can play
|
||||
setTimeout(function () { input.focus(); }, 60);
|
||||
}
|
||||
|
||||
function _close(opts) {
|
||||
opts = opts || {};
|
||||
html.classList.remove('bud-open');
|
||||
btn.classList.remove('active');
|
||||
if (opts.clear !== false) input.value = '';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (html.classList.contains('bud-open')) {
|
||||
_close();
|
||||
} else {
|
||||
_open();
|
||||
}
|
||||
});
|
||||
|
||||
// Escape closes the panel, clears the field
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && html.classList.contains('bud-open')) _close();
|
||||
});
|
||||
|
||||
// Click-outside dismiss — same pattern as game-kit.js
|
||||
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;
|
||||
// Autocomplete suggestions sit outside the panel (panel overflow
|
||||
// is hidden for the scaleX slide). A click inside them must NOT
|
||||
// close+clear the panel.
|
||||
var sg = document.getElementById('id_bud_suggestions');
|
||||
if (sg && sg.contains(e.target)) return;
|
||||
_close();
|
||||
});
|
||||
|
||||
// OK → POST share-post async; reuses the C3.b response handling so the
|
||||
// recipient chip + brief banner + post-line append all light up.
|
||||
// Post-May08b layout: #id_post_table is a <ul> of <li class="post-line">
|
||||
// rows. Author = the sharer (rendered server-side as data-sharer-name on
|
||||
// the panel) so the appended Line matches the post-refresh state, where
|
||||
// the persisted Line.author is request.user.
|
||||
// Author = the sharer (rendered server-side as data-sharer-name) so the
|
||||
// appended Line matches the post-refresh state, where the persisted
|
||||
// Line.author is request.user.
|
||||
function _appendLine(text) {
|
||||
var list = document.getElementById('id_post_table');
|
||||
if (!list) return;
|
||||
@@ -119,18 +59,15 @@
|
||||
li.appendChild(author);
|
||||
li.appendChild(body);
|
||||
li.appendChild(time);
|
||||
// Insert before the trailing buffer if present
|
||||
var buffer = list.querySelector('.post-line-buffer');
|
||||
if (buffer) list.insertBefore(li, buffer);
|
||||
else list.appendChild(li);
|
||||
}
|
||||
|
||||
// The shared-with header lives outside #id_bud_panel — it's two <p>
|
||||
// siblings under .post-header. State transitions:
|
||||
// 0 → 1+ recipients : "just me, X" turns into
|
||||
// "shared between {chip}" + "& me, X"
|
||||
// ≥1 → +1 recipients: append chip + ", " separator before existing
|
||||
// recipient(s).
|
||||
// The shared-with header lives outside #id_bud_panel — two <p> siblings
|
||||
// under .post-header. State transitions:
|
||||
// 0 → 1+ recipients : "just me, X" → "shared between {chip}" + "& me, X"
|
||||
// ≥1 → +1 recipients: append chip + ", " separator before existing.
|
||||
function _appendRecipientChip(displayName) {
|
||||
if (!displayName) return;
|
||||
var header = document.querySelector('.post-page .post-header');
|
||||
@@ -148,56 +85,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 0 → 1+ transition: build the recipients line, rewrite the self
|
||||
// line from "just me, …" to "& me, …".
|
||||
var recipientsLine = document.createElement('p');
|
||||
recipientsLine.className = 'post-shared-recipients';
|
||||
recipientsLine.appendChild(document.createTextNode('shared between '));
|
||||
recipientsLine.appendChild(chip);
|
||||
if (selfLine) {
|
||||
header.insertBefore(recipientsLine, selfLine);
|
||||
// Replace "just me," prefix with "& me,"
|
||||
selfLine.textContent = selfLine.textContent.replace(/^just me,/, '& me,');
|
||||
} else {
|
||||
header.appendChild(recipientsLine);
|
||||
}
|
||||
}
|
||||
|
||||
ok.addEventListener('click', function () {
|
||||
var email = input.value.trim();
|
||||
if (!email) return;
|
||||
|
||||
var fd = new FormData();
|
||||
fd.set('recipient', email);
|
||||
|
||||
fetch(panel.dataset.shareUrl, {
|
||||
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 (data.line_text) _appendLine(data.line_text);
|
||||
if (window.Brief && data.brief) Brief.showBanner(data.brief);
|
||||
if (data.recipient_display) _appendRecipientChip(data.recipient_display);
|
||||
_close({ clear: true });
|
||||
})
|
||||
.catch(function () {
|
||||
// swallow — privacy-safe response shape means even an
|
||||
// unregistered recipient is a 200; only network/5xx land here.
|
||||
});
|
||||
});
|
||||
|
||||
// Submit-on-Enter inside the input mirrors clicking OK
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
ok.click();
|
||||
}
|
||||
bindBudBtn({
|
||||
submitUrl: '{% url "billboard:share_post" post.id %}',
|
||||
autocompleteUrl: '{% url "billboard:search_buds" %}',
|
||||
onSuccess: function (data) {
|
||||
if (data.line_text) _appendLine(data.line_text);
|
||||
if (window.Brief && data.brief) Brief.showBanner(data.brief);
|
||||
if (data.recipient_display) _appendRecipientChip(data.recipient_display);
|
||||
},
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user