PageSourceSearch

https://www.brasil247.com/wp-content/themes/tiebreak/assets/js/b247-user.js?ver=1.0.115

js brasil247.com collected 2026-09-24 08:28:29 UTC 26,081 bytes, 663 lines download raw bytes

1/**
2 * b247-user.js — share popups, save-article toggle, profile dropdown.
3 * Reads runtime config from window.b247User (printed inline by PHP).
4 */
5(function () {
6	'use strict';
7
8	var cfg = window.b247User || {};
9	var i18n = cfg.i18n || {};
10	var savedSet     = new Set((cfg.savedIds || []).map(Number));
11	var followedTerms = new Set((cfg.followedTerms || []).map(Number));
12	var followedAuthors = new Set((cfg.followedAuthors || []).map(Number));
13
14	// ── Save toggle (REST) ───────────────────────────────────────────
15	function rest(method, path, onDone, body) {
16		if (!cfg.restUrl) { onDone({ error: 'no_rest' }); return; }
17		var opts = {
18			method: method,
19			credentials: 'same-origin',
20			headers: { 'X-WP-Nonce': cfg.restNonce, 'Accept': 'application/json' }
21		};
22		if (body) {
23			if (body instanceof FormData) {
24				opts.body = body;
25			} else {
26				opts.body = JSON.stringify(body);
27				opts.headers['Content-Type'] = 'application/json';
28			}
29		}
30		fetch(cfg.restUrl + path.replace(/^\//, ''), opts)
31			.then(function (r) { return r.json().then(function (j) { return { status: r.status, body: j }; }); })
32			.then(function (res) { onDone(res); })
33			.catch(function () { onDone({ error: 'network' }); });
34	}
35
36	function paintSaveBtn(btn, isSaved) {
37		btn.classList.toggle('is-saved', !!isSaved);
38		btn.setAttribute('aria-pressed', isSaved ? 'true' : 'false');
39		btn.setAttribute('aria-label', isSaved ? 'Remover dos salvos' : 'Salvar artigo');
40		var label = btn.querySelector('.b247-save-label');
41		if (label) label.textContent = isSaved ? (i18n.saved || 'Salvo') : (i18n.save || 'Salvar');
42	}
43
44	function handleSaveToggle(btn) {
45		var toolbar = btn.closest('.b247-post-toolbar');
46		var postId = Number((toolbar && toolbar.getAttribute('data-post-id')) || btn.getAttribute('data-post-id') || 0);
47		if (!postId) return;
48
49		if (!cfg.isLoggedIn) {
50			redirectToLogin();
51			return;
52		}
53
54		var currentlySaved = savedSet.has(postId);
55		var nextSaved = !currentlySaved;
56		// Optimistic UI
57		if (nextSaved) savedSet.add(postId); else savedSet.delete(postId);
58		document.querySelectorAll('[data-action="toggle-save"]').forEach(function (b) {
59			var bt = b.closest('.b247-post-toolbar');
60			var bid = Number((bt && bt.getAttribute('data-post-id')) || b.getAttribute('data-post-id') || 0);
61			if (bid === postId) paintSaveBtn(b, nextSaved);
62		});
63		btn.disabled = true;
64
65		rest(nextSaved ? 'POST' : 'DELETE', 'saved/' + postId, function (res) {
66			btn.disabled = false;
67			if (!res || (res.status && res.status >= 400)) {
68				// Roll back on error
69				if (nextSaved) savedSet.delete(postId); else savedSet.add(postId);
70				document.querySelectorAll('[data-action="toggle-save"]').forEach(function (b) {
71					var bt = b.closest('.b247-post-toolbar');
72					var bid = Number((bt && bt.getAttribute('data-post-id')) || b.getAttribute('data-post-id') || 0);
73					if (bid === postId) paintSaveBtn(b, !nextSaved);
74				});
75				return;
76			}
77			// On dashboard, remove the card we just unsaved
78			if (!nextSaved) {
79				var card = btn.closest('.b247-dashboard-card');
80				if (card) card.remove();
81			}
82		});
83	}
84
85	// ── Copy link ─────────────────────────────────────────────────────
86	function handleCopyLink(btn) {
87		var toolbar = btn.closest('.b247-post-toolbar');
88		bumpShareCount(toolbar);
89		var url = toolbar ? toolbar.getAttribute('data-post-url') : getCanonicalUrl();
90		var done = function () {
91			var prev = btn.getAttribute('aria-label');
92			btn.classList.add('is-copied');
93			btn.setAttribute('aria-label', i18n.copied || 'Copiado!');
94			setTimeout(function () {
95				btn.classList.remove('is-copied');
96				btn.setAttribute('aria-label', prev || 'Copiar link');
97			}, 1600);
98		};
99		if (navigator.clipboard && navigator.clipboard.writeText) {
100			navigator.clipboard.writeText(url).then(done, function () {});
101		} else {
102			var ta = document.createElement('textarea');
103			ta.value = url;
104			document.body.appendChild(ta);
105			ta.select();
106			try { document.execCommand('copy'); done(); } catch (e) {}
107			document.body.removeChild(ta);
108		}
109	}
110
111	// ── Share buttons open in popup window for nicer UX ──────────────
112	function handleSharePopup(a) {
113		bumpShareCount(a.closest('.b247-post-toolbar'));
114		var url = a.getAttribute('href');
115		if (!url) return;
116		var w = Math.min(640, window.innerWidth - 40);
117		var h = Math.min(560, window.innerHeight - 40);
118		var x = (window.outerWidth - w) / 2 + (window.screenX || 0);
119		var y = (window.outerHeight - h) / 2 + (window.screenY || 0);
120		window.open(url, 'b247share', 'width=' + w + ',height=' + h + ',left=' + x + ',top=' + y + ',noopener,noreferrer');
121	}
122
123	// ── Share counter: global per-post tally, this browser caps at 5 ──
124	var SHARE_CAP = 5;
125
126	function formatInt(n) {
127		try { return Number(n).toLocaleString('pt-BR'); } catch (e) { return String(n); }
128	}
129
130	function paintShareCount(el, n) {
131		if (!el) return;
132		var num = el.querySelector('.b247-share-count-num');
133		if (num) num.textContent = formatInt(n);
134		el.classList.toggle('is-empty', !(n > 0));
135	}
136
137	// Count any share/copy click. Optimistically +1, then reconcile to the
138	// authoritative total the endpoint returns. localStorage caps this browser's
139	// contribution at 5 per post (server enforces the same cap per IP).
140	function bumpShareCount(toolbar) {
141		if (!toolbar) return;
142		var pid = Number(toolbar.getAttribute('data-post-id') || 0);
143		if (!pid) return;
144
145		var key = 'b247_sc_' + pid;
146		var mine = 0;
147		try { mine = parseInt(localStorage.getItem(key) || '0', 10) || 0; } catch (e) {}
148		if (mine >= SHARE_CAP) return;
149
150		var el = toolbar.querySelector('.b247-share-count');
151		if (el) {
152			var num = el.querySelector('.b247-share-count-num');
153			var current = num ? (parseInt(String(num.textContent).replace(/\D/g, ''), 10) || 0) : 0;
154			paintShareCount(el, current + 1);
155		}
156		try { localStorage.setItem(key, String(mine + 1)); } catch (e) {}
157
158		rest('POST', 'share/' + pid, function (res) {
159			if (el && res && res.body && typeof res.body.count !== 'undefined') {
160				paintShareCount(el, res.body.count);
161			}
162		});
163	}
164
165	function safeUrl(raw, fallback) {
166		var defaultUrl = fallback === undefined ? '#' : fallback;
167		if (!raw) return defaultUrl;
168		try {
169			return new URL(String(raw), document.baseURI || '/').href;
170		} catch (e) {
171			return defaultUrl;
172		}
173	}
174
175	function getCanonicalUrl() {
176		var canonical = document.querySelector('link[rel="canonical"]');
177		return canonical && canonical.href ? canonical.href : '';
178	}
179
180	function redirectToLogin() {
181		window.open(safeUrl(cfg.loginUrl, '/wp-login.php'), '_self');
182	}
183
184	function createEl(tag, className, text) {
185		var el = document.createElement(tag);
186		if (className) el.className = className;
187		if (text !== undefined && text !== null) el.textContent = String(text);
188		return el;
189	}
190
191	function createSvg(pathData) {
192		var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
193		var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
194		svg.setAttribute('viewBox', '0 0 24 24');
195		svg.setAttribute('aria-hidden', 'true');
196		svg.setAttribute('focusable', 'false');
197		path.setAttribute('d', pathData);
198		svg.appendChild(path);
199		return svg;
200	}
201
202	function createCta(className, href, text) {
203		var link = createEl('a', className, text);
204		link.href = safeUrl(href, '#');
205		return link;
206	}
207
208	function createSavedEmptyItem() {
209		return createEl('li', 'b247-profile-empty', i18n.noSaved || 'Nada salvo.');
210	}
211
212	function createSavedItem(item) {
213		var li = document.createElement('li');
214		var link = document.createElement('a');
215		link.href = safeUrl(item && item.link, '#');
216		if (item && item.image) {
217			var img = document.createElement('img');
218			img.src = safeUrl(item.image, '');
219			img.alt = '';
220			img.loading = 'lazy';
221			link.appendChild(img);
222		} else {
223			link.appendChild(createEl('span', 'b247-profile-thumb-empty'));
224		}
225		link.appendChild(createEl('span', '', (item && item.title) || ''));
226		li.appendChild(link);
227		return li;
228	}
229
230		// ── Profile dropdown ──────────────────────────────────────────────
231		function buildProfilePanel(panel) {
232			var dashUrl = cfg.dashboardUrl || '/minha-conta/';
233			panel.textContent = '';
234			if (cfg.isLoggedIn) {
235				var feedUrl = cfg.customFeedUrl || (dashUrl + '#b247-d-para-voce');
236
237				var head = createEl('header', 'b247-profile-head');
238				if (cfg.avatarUrl) {
239					var avatar = document.createElement('img');
240					avatar.className = 'b247-profile-avatar';
241					avatar.src = safeUrl(cfg.avatarUrl, '');
242					avatar.alt = '';
243					head.appendChild(avatar);
244				}
245				var identity = document.createElement('div');
246				identity.appendChild(createEl('strong', '', cfg.displayName || ''));
247				identity.appendChild(createCta('b247-profile-logout', cfg.logoutUrl, i18n.logout || 'Sair'));
248				head.appendChild(identity);
249				panel.appendChild(head);
250
251				var saved = createEl('div', 'b247-profile-saved');
252				saved.appendChild(createEl('h4', '', i18n.saved || 'Salvos'));
253				var list = createEl('ul', 'b247-profile-saved-list');
254				list.setAttribute('aria-live', 'polite');
255				list.appendChild(createEl('li', 'b247-profile-loading', String.fromCharCode(0x2026)));
256				saved.appendChild(list);
257				panel.appendChild(saved);
258
259				var ctas = createEl('div', 'b247-profile-ctas');
260				var feed = createCta('b247-profile-cta b247-profile-cta--feed', feedUrl, '');
261				feed.appendChild(createSvg('M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6H6zm7 1.5L18.5 9H14a1 1 0 0 1-1-1V3.5zM8 13h8v2H8v-2zm0 4h5v2H8v-2z'));
262				feed.appendChild(createEl('span', '', i18n.customFeed || 'Meu feed'));
263				ctas.appendChild(feed);
264				ctas.appendChild(createCta('b247-profile-cta', dashUrl, (i18n.viewAll || 'Ver dashboard completo') + ' ' + String.fromCharCode(0x203A)));
265				panel.appendChild(ctas);
266				loadSavedList(list);
267			} else {
268				var guest = createEl('header', 'b247-profile-head b247-profile-head--guest');
269				guest.appendChild(createSvg('M12 12c2.7 0 4.9-2.2 4.9-4.9S14.7 2.2 12 2.2 7.1 4.4 7.1 7.1 9.3 12 12 12zm0 2.4c-3.3 0-9.9 1.6-9.9 4.9v2.4h19.8v-2.4c0-3.3-6.6-4.9-9.9-4.9z'));
270				guest.appendChild(createEl('p', '', i18n.loginNeeded || 'Faça login para salvar artigos.'));
271				panel.appendChild(guest);
272				panel.appendChild(createCta('b247-profile-cta b247-profile-cta--primary', cfg.loginUrl, i18n.login || 'Entrar'));
273				if (cfg.registerUrl) {
274					panel.appendChild(createCta('b247-profile-cta', cfg.registerUrl, i18n.register || 'Criar conta'));
275				}
276			}
277		}
278
279	function loadSavedList(ul) {
280			if (!ul) return;
281			rest('GET', 'saved', function (res) {
282				ul.textContent = '';
283				if (!res || !res.body || !res.body.items) {
284					ul.appendChild(createSavedEmptyItem());
285					return;
286				}
287				var items = res.body.items.slice(0, 5);
288				if (items.length === 0) {
289					ul.appendChild(createSavedEmptyItem());
290					return;
291				}
292				items.forEach(function (it) {
293					ul.appendChild(createSavedItem(it));
294				});
295			});
296		}
297
298	function setupProfileDropdown() {
299		var btn = document.getElementById('b247-profile-btn');
300		var panel = document.getElementById('b247-profile-panel');
301		if (!btn || !panel) return;
302
303		var built = false;
304		function open() {
305			if (!built) { buildProfilePanel(panel); built = true; }
306			panel.hidden = false;
307			btn.setAttribute('aria-expanded', 'true');
308			document.addEventListener('click', onDocClick, true);
309			document.addEventListener('keydown', onEsc, true);
310		}
311		function close() {
312			panel.hidden = true;
313			btn.setAttribute('aria-expanded', 'false');
314			document.removeEventListener('click', onDocClick, true);
315			document.removeEventListener('keydown', onEsc, true);
316		}
317		function onDocClick(e) {
318			if (panel.contains(e.target) || btn.contains(e.target)) return;
319			close();
320		}
321		function onEsc(e) { if (e.key === 'Escape') close(); }
322
323		btn.addEventListener('click', function () {
324			if (panel.hidden) open(); else close();
325		});
326	}
327
328	// ── Follow / unfollow buttons (term + author) ─────────────────────
329	function paintFollowBtn(btn, isFollowing) {
330		btn.classList.toggle('is-following', !!isFollowing);
331		btn.setAttribute('aria-pressed', isFollowing ? 'true' : 'false');
332		var label = btn.querySelector('.b247-follow-label');
333		var icon  = btn.querySelector('.b247-follow-icon');
334		if (label) {
335			var base = label.getAttribute('data-base-label') || label.textContent;
336			if (!label.getAttribute('data-base-label')) { label.setAttribute('data-base-label', base); }
337			label.textContent = isFollowing ? (i18n.following || 'Seguindo') : base;
338		}
339		if (icon) icon.textContent = isFollowing ? '✓' : '+';
340	}
341
342	function handleFollow(btn) {
343		var type = btn.getAttribute('data-follow-type');
344		var id   = Number(btn.getAttribute('data-follow-id') || 0);
345		if (!id || (type !== 'term' && type !== 'author')) return;
346
347		if (!cfg.isLoggedIn) {
348			redirectToLogin();
349			return;
350		}
351
352		var set = type === 'term' ? followedTerms : followedAuthors;
353		var nowFollowing = !set.has(id);
354		if (nowFollowing) set.add(id); else set.delete(id);
355		document.querySelectorAll('.b247-follow-btn[data-follow-type="' + type + '"][data-follow-id="' + id + '"]').forEach(function (b) {
356			paintFollowBtn(b, nowFollowing);
357		});
358		btn.disabled = true;
359		rest(nowFollowing ? 'POST' : 'DELETE', 'follow/' + type + '/' + id, function (res) {
360			btn.disabled = false;
361			if (!res || (res.status && res.status >= 400)) {
362				// roll back
363				if (nowFollowing) set.delete(id); else set.add(id);
364				document.querySelectorAll('.b247-follow-btn[data-follow-type="' + type + '"][data-follow-id="' + id + '"]').forEach(function (b) {
365					paintFollowBtn(b, !nowFollowing);
366				});
367			}
368		});
369	}
370
371	function handleUnfollowChip(btn) {
372		var type = btn.getAttribute('data-follow-type');
373		var id   = Number(btn.getAttribute('data-follow-id') || 0);
374		if (!id || (type !== 'term' && type !== 'author')) return;
375		var set = type === 'term' ? followedTerms : followedAuthors;
376		set.delete(id);
377		var li = btn.closest('li');
378		if (li) li.remove();
379		rest('DELETE', 'follow/' + type + '/' + id, function () {});
380	}
381
382	// ── History clear ────────────────────────────────────────────────
383	function handleClearHistory(btn) {
384		if (!window.confirm('Limpar todo o seu histórico de leitura?')) return;
385		btn.disabled = true;
386		rest('DELETE', 'history', function (res) {
387			btn.disabled = false;
388			if (res && res.body && res.body.ok) {
389				var sec = document.getElementById('b247-d-historico');
390				if (sec) {
391					var list = sec.querySelector('.b247-dashboard-list');
392					if (list) list.remove();
393					var head = sec.querySelector('.b247-dashboard-section-head .b247-link-btn');
394					if (head) head.remove();
395					var empty = document.createElement('p');
396					empty.className = 'b247-dashboard-empty';
397					empty.textContent = 'Os últimos artigos que você leu aparecerão aqui.';
398					sec.appendChild(empty);
399				}
400			}
401		});
402	}
403
404	// ── Comments filter tabs ─────────────────────────────────────────
405	function handleCommentFilter(btn) {
406		var tabs = btn.parentElement.querySelectorAll('.b247-filter-tab');
407		tabs.forEach(function (t) { t.classList.remove('is-active'); });
408		btn.classList.add('is-active');
409		var status = btn.getAttribute('data-filter-status');
410		var section = btn.closest('section');
411		if (!section) return;
412		section.querySelectorAll('[data-comments-status]').forEach(function (ul) {
413			ul.hidden = ul.getAttribute('data-comments-status') !== status;
414		});
415	}
416
417	// ── Dashboard forms (profile / password / privacy) ───────────────
418	function setFormStatus(form, message, isError) {
419		var el = form.querySelector('.b247-form-status');
420		if (!el) return;
421		el.textContent = message || '';
422		el.classList.toggle('is-error', !!isError);
423		el.classList.toggle('is-success', !!message && !isError);
424	}
425
426	function handleFormSubmit(form) {
427		var kind = form.getAttribute('data-form');
428		setFormStatus(form, '', false);
429
430		if (kind === 'profile') {
431			var fd = new FormData(form);
432			form.querySelector('button[type="submit"]').disabled = true;
433			rest('POST', 'profile', function (res) {
434				form.querySelector('button[type="submit"]').disabled = false;
435				if (res && res.body && res.body.ok) {
436					setFormStatus(form, i18n.saved_ok || 'Salvo!', false);
437					if (res.body.avatar_url) {
438						document.querySelectorAll('.b247-dashboard-avatar').forEach(function (img) { img.src = res.body.avatar_url; });
439					}
440				} else {
441					var msg = (res && res.body && res.body.error) || (i18n.error || 'Erro');
442					setFormStatus(form, msg, true);
443				}
444			}, fd);
445			return;
446		}
447
448		if (kind === 'password') {
449			var pwNew     = form.querySelector('[name="new_password"]').value;
450			var pwConfirm = form.querySelector('[name="new_password_confirm"]').value;
451			if (pwNew !== pwConfirm) {
452				setFormStatus(form, 'As senhas não coincidem.', true);
453				return;
454			}
455			form.querySelector('button[type="submit"]').disabled = true;
456			rest('POST', 'password', function (res) {
457				form.querySelector('button[type="submit"]').disabled = false;
458				if (res && res.body && res.body.ok) {
459					form.reset();
460					setFormStatus(form, 'Senha atualizada.', false);
461				} else {
462					var err = res && res.body && res.body.error;
463					var msg = err === 'wrong_current' ? 'Senha atual incorreta.'
464						: err === 'weak_password' ? 'A nova senha precisa ter pelo menos 8 caracteres.'
465						: (i18n.error || 'Erro');
466					setFormStatus(form, msg, true);
467				}
468			}, {
469				current: form.querySelector('[name="current"]').value,
470				new_password: pwNew
471			});
472			return;
473		}
474
475		if (kind === 'privacy') {
476			var body = {
477				analytics: form.querySelector('[name="analytics"]').checked ? 1 : 0,
478				marketing: form.querySelector('[name="marketing"]').checked ? 1 : 0,
479				lgpd_ack:  form.querySelector('[name="lgpd_ack"]').checked  ? 1 : 0
480			};
481			form.querySelector('button[type="submit"]').disabled = true;
482			rest('POST', 'privacy', function (res) {
483				form.querySelector('button[type="submit"]').disabled = false;
484				if (res && res.body && res.body.ok) {
485					setFormStatus(form, 'Preferências salvas.', false);
486				} else {
487					setFormStatus(form, i18n.error || 'Erro', true);
488				}
489			}, body);
490			return;
491		}
492	}
493
494	// ── Card card actions menu (3-dot) ──────────────────────────────────────
495	var openCardActions = null;
496	function closeCardActions() {
497		if (!openCardActions) return;
498		var menu = openCardActions.querySelector('.b247-card-actions-menu');
499		var btn  = openCardActions.querySelector('.b247-card-actions-toggle');
500		if (menu) menu.hidden = true;
501		if (btn)  btn.setAttribute('aria-expanded', 'false');
502		openCardActions.classList.remove('is-open');
503		openCardActions = null;
504		document.removeEventListener('click', onCardActionsDocClick, true);
505		document.removeEventListener('keydown', onCardActionsEsc, true);
506	}
507	function onCardActionsDocClick(e) {
508		if (openCardActions && !openCardActions.contains(e.target)) closeCardActions();
509	}
510	function onCardActionsEsc(e) { if (e.key === 'Escape') closeCardActions(); }
511
512	function toggleCardActions(btn) {
513		var wrap = btn.closest('.b247-card-actions');
514		if (!wrap) return;
515		if (openCardActions && openCardActions !== wrap) closeCardActions();
516		var menu = wrap.querySelector('.b247-card-actions-menu');
517		var isOpen = !menu.hidden;
518		menu.hidden = isOpen;
519		btn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
520		wrap.classList.toggle('is-open', !isOpen);
521		if (!isOpen) {
522			openCardActions = wrap;
523			document.addEventListener('click', onCardActionsDocClick, true);
524			document.addEventListener('keydown', onCardActionsEsc, true);
525		} else {
526			openCardActions = null;
527			document.removeEventListener('click', onCardActionsDocClick, true);
528			document.removeEventListener('keydown', onCardActionsEsc, true);
529		}
530	}
531
532	function handleCardActionsItem(item) {
533		var wrap   = item.closest('.b247-card-actions');
534		if (!wrap) return;
535		var action = item.getAttribute('data-action');
536		var postId = Number(wrap.getAttribute('data-post-id') || 0);
537		var url    = wrap.getAttribute('data-post-url') || '';
538		var authorId = Number(wrap.getAttribute('data-author-id') || 0);
539
540		if (action === 'card-action-save') {
541			if (!cfg.isLoggedIn) { redirectToLogin(); return; }
542			var nowSaved = !savedSet.has(postId);
543			if (nowSaved) savedSet.add(postId); else savedSet.delete(postId);
544			rest(nowSaved ? 'POST' : 'DELETE', 'saved/' + postId, function () {});
545			// Visual confirmation in the menu item
546			var sp = item.querySelector('span');
547			if (sp) sp.textContent = nowSaved ? (i18n.saved_ok || 'Salvo!') : 'Salvar para ler depois';
548			setTimeout(closeCardActions, 700);
549			return;
550		}
551		if (action === 'card-action-share-wa' || action === 'card-action-share-x') {
552			var href = item.getAttribute('data-href');
553			if (href) {
554				var w = Math.min(640, window.innerWidth - 40);
555				var h = Math.min(560, window.innerHeight - 40);
556				window.open(href, 'b247share', 'width=' + w + ',height=' + h + ',noopener,noreferrer');
557			}
558			closeCardActions();
559			return;
560		}
561		if (action === 'card-action-copy') {
562			if (navigator.clipboard && navigator.clipboard.writeText) {
563				navigator.clipboard.writeText(url).then(function () {
564					var sp = item.querySelector('span');
565					if (sp) sp.textContent = i18n.copied || 'Copiado!';
566					setTimeout(closeCardActions, 700);
567				});
568			} else { closeCardActions(); }
569			return;
570		}
571		if (action === 'card-action-follow-author') {
572			if (!cfg.isLoggedIn) { redirectToLogin(); return; }
573			var aid = Number(item.getAttribute('data-follow-id') || authorId);
574			var nowFollowing = !followedAuthors.has(aid);
575			if (nowFollowing) followedAuthors.add(aid); else followedAuthors.delete(aid);
576			rest(nowFollowing ? 'POST' : 'DELETE', 'follow/author/' + aid, function () {});
577			var sp2 = item.querySelector('span');
578			if (sp2) sp2.textContent = nowFollowing ? (i18n.following || 'Seguindo') : 'Seguir autor';
579			setTimeout(closeCardActions, 700);
580			return;
581		}
582	}
583
584	// ── Wire everything up on DOMContentLoaded ────────────────────────
585	function init() {
586		document.addEventListener('click', function (e) {
587			var saveBtn = e.target.closest && e.target.closest('[data-action="toggle-save"]');
588			if (saveBtn) { e.preventDefault(); handleSaveToggle(saveBtn); return; }
589
590			var copyBtn = e.target.closest && e.target.closest('[data-action="copy-link"]');
591			if (copyBtn) { e.preventDefault(); handleCopyLink(copyBtn); return; }
592
593			var share = e.target.closest && e.target.closest('.b247-share-btn[href]');
594			if (share) {
595				e.preventDefault();
596				handleSharePopup(share);
597				return;
598			}
599
600			// Only intercept real follow toggles (author/category). The follow
601			// cluster reuses .b247-follow-btn for the Apoie, Newsletter and
602			// Google News links too — those need their default click behaviour.
603			var followBtn = e.target.closest && e.target.closest('.b247-follow-btn[data-follow-type]');
604			if (followBtn) { e.preventDefault(); handleFollow(followBtn); return; }
605
606			var chipX = e.target.closest && e.target.closest('.b247-chip-x[data-action="unfollow"]');
607			if (chipX) { e.preventDefault(); handleUnfollowChip(chipX); return; }
608
609			var clearHist = e.target.closest && e.target.closest('[data-action="clear-history"]');
610			if (clearHist) { e.preventDefault(); handleClearHistory(clearHist); return; }
611
612			var filterTab = e.target.closest && e.target.closest('.b247-filter-tab');
613			if (filterTab) { e.preventDefault(); handleCommentFilter(filterTab); return; }
614
615			var actionsBtn = e.target.closest && e.target.closest('.b247-card-actions-toggle');
616			if (actionsBtn) { e.preventDefault(); e.stopPropagation(); toggleCardActions(actionsBtn); return; }
617
618			var actionsItem = e.target.closest && e.target.closest('.b247-card-actions-item');
619			if (actionsItem) { e.preventDefault(); handleCardActionsItem(actionsItem); return; }
620		});
621
622		document.addEventListener('submit', function (e) {
623			var form = e.target.closest && e.target.closest('.b247-dashboard-form');
624			if (!form) return;
625			e.preventDefault();
626			handleFormSubmit(form);
627		});
628
629		setupProfileDropdown();
630
631		// Painting the initial follow / save indicators is non-critical — the
632		// delegated click/submit handlers above already carry every interaction.
633		// On a listing with ~30 cards these two sweeps are 60-120 synchronous DOM
634		// mutations; running them at idle keeps them out of the load / first-tap
635		// INP window. The state simply appears a few hundred ms later.
636		var paintInitialStates = function () {
637			// Follow-button states based on cfg.
638			document.querySelectorAll('.b247-follow-btn').forEach(function (btn) {
639				var type = btn.getAttribute('data-follow-type');
640				var id   = Number(btn.getAttribute('data-follow-id') || 0);
641				var set  = type === 'term' ? followedTerms : (type === 'author' ? followedAuthors : null);
642				if (set) paintFollowBtn(btn, set.has(id));
643			});
644			// Toolbar save buttons whose state may be stale (e.g., page from cache).
645			document.querySelectorAll('.b247-post-toolbar').forEach(function (tb) {
646				var pid = Number(tb.getAttribute('data-post-id') || 0);
647				var btn = tb.querySelector('[data-action="toggle-save"]');
648				if (btn) paintSaveBtn(btn, savedSet.has(pid));
649			});
650		};
651		if ('requestIdleCallback' in window) {
652			requestIdleCallback(paintInitialStates, { timeout: 2000 });
653		} else {
654			setTimeout(paintInitialStates, 200);
655		}
656	}
657
658	if (document.readyState === 'loading') {
659		document.addEventListener('DOMContentLoaded', init);
660	} else {
661		init();
662	}
663}());

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.