' + beacon;
}function renderSandboxed(container, html, size, adId) {
size = size || { w: 336, h: 280 };
applyFixedSize(container, size);
var iframe = document.createElement('iframe');
iframe.className = 'fca-ad__sandbox';
iframe.setAttribute('sandbox', 'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-presentation allow-forms');
iframe.setAttribute('referrerpolicy', 'no-referrer');
iframe.setAttribute('loading', 'lazy');
iframe.setAttribute('title', 'Advertisement');
if (adId) { iframe.setAttribute('data-ad-id', String(adId)); }
iframe.width = String(size.w || 336);
iframe.height = String(size.h || 280);
iframe.style.display = 'block';
iframe.style.width = '100%';
iframe.style.height = (size.h || 280) + 'px';
iframe.style.border = '0';
iframe.style.overflow = 'hidden';
iframe.style.background = 'transparent';
iframe.srcdoc = clippedSrcdoc(html, size, adId);
container.appendChild(iframe);
}// ── member reporting ───────────────────────────────────────
// A small "Report" affordance on served ads. Opens a lightweight
// popover asking for an optional reason, then POSTs to /report.
function closeReportPopover() {
if (window.__fcaReportPop && window.__fcaReportPop.parentNode) {
window.__fcaReportPop.parentNode.removeChild(window.__fcaReportPop);
}
window.__fcaReportPop = null;
document.removeEventListener('click', onDocClickForReport, true);
}
function onDocClickForReport(e) {
var pop = window.__fcaReportPop;
if (pop && !pop.contains(e.target)) { closeReportPopover(); }
}
function positionReportPopover(pop, anchor) {
var r = anchor.getBoundingClientRect();
var w = pop.offsetWidth || 260;
var left = Math.min(Math.max(8, r.right - w), window.innerWidth - w - 8);
var top = r.bottom + 6;
if (top + pop.offsetHeight > window.innerHeight - 8) {
top = Math.max(8, r.top - pop.offsetHeight - 6);
}
pop.style.left = (left + window.scrollX) + 'px';
pop.style.top = (top + window.scrollY) + 'px';
}
// Remember ads this browser has already reported so the control is
// hidden immediately (server still enforces one-per-user).
function reportedSet() {
try { return JSON.parse(window.localStorage.getItem('fca_reported_ads') || '[]') || []; }
catch (e) { return []; }
}
function hasReported(id) {
return reportedSet().indexOf(Number(id)) !== -1;
}
function markReported(id) {
try {
var s = reportedSet();
if (s.indexOf(Number(id)) === -1) {
s.push(Number(id));
window.localStorage.setItem('fca_reported_ads', JSON.stringify(s));
}
} catch (e) { /* ignore */ }
}
function submitReport(adId, reason) {
return fetch(apiUrl('report'), {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': CFG.nonce },
body: JSON.stringify({ ad_id: adId, reason: reason || '' })
}).then(function (r) {
return r.json().then(function (d) {
return { ok: r.ok, status: r.status, data: d || {} };
}).catch(function () {
return { ok: r.ok, status: r.status, data: {} };
});
}).catch(function () {
return { ok: false, status: 0, data: {} };
});
}
function openReportPopover(anchor, ad) {
closeReportPopover();
var pop = document.createElement('div');
pop.className = 'fca-ad-report-pop';
pop.innerHTML =
'
Report this ad
' +
'' +
'
' +
'' +
'' +
'
';
document.body.appendChild(pop);
window.__fcaReportPop = pop;
positionReportPopover(pop, anchor);
var ta = pop.querySelector('.fca-ad-report-pop__reason');
if (ta) { ta.focus(); }
pop.querySelector('.fca-ad-report-pop__cancel').addEventListener('click', closeReportPopover);
pop.querySelector('.fca-ad-report-pop__send').addEventListener('click', function () {
var btn = this;
btn.disabled = true; btn.textContent = 'Sending\u2026';
submitReport(ad.id, ta ? ta.value : '').then(function (res) {
var already = res.data && res.data.code === 'fca_ads_already_reported';
var done = res.ok || already;
var msg = res.ok
? 'Thanks \u2014 this ad has been reported.'
: (already
? 'You have already reported this ad.'
: (res.status === 401
? 'Please log in to report ads.'
: 'Could not send your report.'));
pop.innerHTML = '
' + msg + '
';
if (done) {
markReported(ad.id);
// Lock every Report control for this ad into the
// reported state so the viewer sees it's recorded.
lockReportedButton(anchor);
var sel = document.querySelectorAll('.fca-ad__report');
Array.prototype.forEach.call(sel, function (el) {
if (el !== anchor && el.closest && el.closest('[data-ad-id="' + ad.id + '"]')) {
lockReportedButton(el);
}
});
}
setTimeout(closeReportPopover, done ? 1600 : 2400);
});
});
setTimeout(function () {
document.addEventListener('click', onDocClickForReport, true);
}, 0);
}
var REPORT_FLAG_SVG = '';
var REPORT_DONE_SVG = '';
// Lock a report button into the "already reported" state (colored,
// disabled) so the viewer sees their report was recorded.
function lockReportedButton(btn) {
if (!btn) { return; }
btn.disabled = true;
if (btn.className.indexOf('fca-ad__report--reported') === -1) {
btn.className = (btn.className + ' fca-ad__report--reported').trim();
}
btn.title = 'You already reported this ad';
btn.setAttribute('aria-label', 'You already reported this ad');
btn.innerHTML = REPORT_DONE_SVG;
}
function addReportControl(wrap, ad, serveCfg) {
if (!serveCfg || !serveCfg.reportsEnabled || !serveCfg.canReport) { return; }
if (!ad || !ad.id || ad.demo) { return; }
var rb = document.createElement('button');
rb.type = 'button';
rb.className = 'fca-ad__report';
wrap.classList.add('fca-ad--reportable');if (hasReported(ad.id)) {
lockReportedButton(rb);
wrap.appendChild(rb);
return;
}rb.title = 'Report this ad';
rb.setAttribute('aria-label', 'Report this ad');
rb.innerHTML = REPORT_FLAG_SVG;
rb.addEventListener('click', function (e) {
e.preventDefault(); e.stopPropagation();
openReportPopover(rb, ad);
});
wrap.appendChild(rb);
}// NOTE: custom third-party ad code is ALWAYS rendered inside a
// sandboxed iframe (renderSandboxed) so its scripts run in an opaque
// origin and can never touch the portal DOM/cookies. We deliberately
// do NOT provide any "inject + re-execute scripts into the page"
// helper — that would be a stored-XSS primitive.// ── external libs ──────────────────────────────────────────
function loadAdSense(client) {
if (state.adsenseLoaded || !client) { return; }
state.adsenseLoaded = true;
var s = document.createElement('script');
s.async = true;
s.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=' + encodeURIComponent(client);
s.crossOrigin = 'anonymous';
document.head.appendChild(s);
}function loadGpt() {
if (state.gptLoaded) { return; }
state.gptLoaded = true;
var s = document.createElement('script');
s.async = true;
s.src = 'https://securepubads.g.doubleclick.net/tag/js/gpt.js';
document.head.appendChild(s);
window.googletag = window.googletag || { cmd: [] };
}// ── impression tracking ────────────────────────────────────
var io = null;
function observer() {
if (io) { return io; }
if (!('IntersectionObserver' in window)) { return null; }
io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting && en.intersectionRatio >= 0.5) {
var el = en.target;
if (!el.__fcaSeen) {
el.__fcaSeen = true;
track(el.getAttribute('data-ad-id'), 'impression', el.__fcaServeCfg);
io.unobserve(el);
}
}
});
}, { threshold: [0.5] });
return io;
}function watchImpression(el, serveCfg) {
el.__fcaServeCfg = serveCfg;
var ob = observer();
if (ob) { ob.observe(el); }
else { track(el.getAttribute('data-ad-id'), 'impression', serveCfg); }
}// ── demo placeholders ──────────────────────────────────────
// In demo mode we never call the ad networks; we render a sized
// box so the community owner can preview each slot's footprint.
function adsenseSize(format) {
switch (format) {
case 'rectangle': return { w: 300, h: 250 };
case 'horizontal': return { w: 660, h: 90 }; // FC feed column (≤680)
case 'vertical': return { w: 160, h: 600 };
case 'fluid': return { responsive: true, h: 200 };
default: return { responsive: true, h: 120 };
}
}
function firstSize(sizes) {
if (sizes && sizes.length) { return { w: sizes[0][0], h: sizes[0][1] }; }
return { responsive: true, h: 250 };
}
// Human-readable placement labels so a demo placeholder tells the
// admin which zone it belongs to (mirrors zone_definitions()).
var LOC_LABELS = {
'header': 'Header (top banner)',
'feed': 'Between feed posts',
'sidebar': 'Right sidebar',
'sidebar-left': 'Left sidebar (navigation)',
'post-before': 'Post \u2014 before content',
'post-inside': 'Post \u2014 inside content',
'post-after': 'Post \u2014 after content',
'event-list': 'Events \u2014 listing',
'event-single-before': 'Event \u2014 before content',
'event-single-inside': 'Event \u2014 inside content',
'event-single-after': 'Event \u2014 after content'
};
function locationLabel(kind) {
if (!kind) { return ''; }
return LOC_LABELS[kind] || kind.replace(/-/g, ' ');
}
function renderPlaceholder(body, label, size, kind) {
size = size || { responsive: true, h: 120 };
var box = document.createElement('div');
box.className = 'fca-ad__placeholder';
if (size.responsive) {
box.className += ' fca-ad__placeholder--responsive';
box.style.minHeight = (size.h || 120) + 'px';
} else {
box.style.width = size.w + 'px';
box.style.height = size.h + 'px';
box.style.maxWidth = '100%';
}
var name = document.createElement('span');
name.className = 'fca-ad__placeholder-label';
var loc = locationLabel(kind);
name.textContent = 'FCA Ads · ' + (loc ? (loc + ' · ') : '') + label + ' · demo';
var dim = document.createElement('span');
dim.className = 'fca-ad__placeholder-size';
dim.textContent = size.responsive ? 'responsive' : (size.w + '\u00d7' + size.h);
box.appendChild(name);
box.appendChild(dim);
body.appendChild(box);
}// ── ad element builders ────────────────────────────────────
function buildShell(kind, ad, serveCfg) {
var wrap = document.createElement('div');
var isSide = (kind === 'sidebar' || kind === 'sidebar-left');
wrap.className = (isSide ? 'app_side_widget ' : '') + 'fca-ad fca-ad--' + kind + ' fca-ad--fixed';
wrap.setAttribute(TAG, kind);
wrap.setAttribute('data-ad-id', ad.id);
var size = sizeForKind(kind, ad);
wrap.setAttribute('data-fca-w', String(size.w || 336));
wrap.setAttribute('data-fca-h', String(size.h || 280));
if (size.key) { wrap.setAttribute('data-fca-size', size.key); }
if (serveCfg && serveCfg.showSponsored) {
var lbl = document.createElement('span');
lbl.className = 'fca-ad__label';
lbl.textContent = serveCfg.sponsoredLabel || 'Sponsored';
wrap.appendChild(lbl);
}
var body = document.createElement('div');
body.className = 'fca-ad__body';
applyFixedSize(body, size);
wrap.appendChild(body);
addReportControl(wrap, ad, serveCfg);
return { wrap: wrap, body: body };
}// Fill the body AFTER the wrapper is attached to the DOM (AdSense
// / GPT require the slot element to be in-document before push).
function activate(ad, body, serveCfg, kind) {
if (ad.type === 'adsense') {
var a = ad.adsense || {};
if (serveCfg && serveCfg.demoMode) {
renderPlaceholder(body, 'AdSense', adsenseSize(a.format), kind);
return true;
}
if (!a.client) { return false; }
loadAdSense(a.client);
if (serveCfg && serveCfg.adsenseAutoAds) {
if (!state.autoAdsPushed) {
state.autoAdsPushed = true;
try {
(window.adsbygoogle = window.adsbygoogle || []).push({
google_ad_client: a.client, enable_page_level_ads: true
});
} catch (e) {}
}
return false; // Auto Ads place themselves; drop the manual shell.
}
var ins = document.createElement('ins');
ins.className = 'adsbygoogle';
ins.style.display = 'block';
ins.setAttribute('data-ad-client', a.client);
if (a.slot) { ins.setAttribute('data-ad-slot', a.slot); }
ins.setAttribute('data-ad-format', a.format || 'auto');
if (a.responsive) { ins.setAttribute('data-full-width-responsive', 'true'); }
body.appendChild(ins);
try { (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) {}
return true;
}if (ad.type === 'gam') {
var g = ad.gam || {};
if (serveCfg && serveCfg.demoMode) {
renderPlaceholder(body, 'Ad Manager', firstSize(parseSizes(g.sizes)), kind);
return true;
}
if (!g.unitPath) { return false; }
loadGpt();
var sizes = parseSizes(g.sizes);
var divId = 'fca-gpt-' + ad.id + '-' + Math.random().toString(36).slice(2, 8);
var slotDiv = document.createElement('div');
slotDiv.id = divId;
body.appendChild(slotDiv);
window.googletag = window.googletag || { cmd: [] };
window.googletag.cmd.push(function () {
var slot = window.googletag.defineSlot(g.unitPath, sizes.length ? sizes : ['fluid'], divId);
if (slot) {
slot.addService(window.googletag.pubads());
window.googletag.enableServices();
window.googletag.display(divId);
}
});
return true;
}if (ad.type === 'custom') {
var c = ad.custom || {};
if (!c.code) { return false; }
renderSandboxed(body, c.code, sizeForKind(kind || 'feed', ad), ad.id);
return true;
}// house creative — keep body at the resolved footprint
applyFixedSize(body, sizeForKind(kind || 'feed', ad));
var h = ad.house || {};
var fmt = h.format || 'both';
var link = document.createElement('a');
link.className = 'fca-ad__house';
link.href = safeUrl(h.linkUrl) || '#';
link.target = safeTarget(h.target);
link.rel = 'noopener sponsored';
var bg = safeHexColor(h.bgColor);
if (bg) { link.style.backgroundColor = bg; }
var inner = '';
var imgUrl = safeUrl(h.imageUrl);
if (fmt !== 'copy' && imgUrl) {
inner += '';
}
var titleText = h.headline || ad.title || '';
if (fmt !== 'image' && titleText) { inner += '
' + escapeHtml(titleText) + '
'; }
if (fmt !== 'image' && h.body) {
// Body is server-side wp_kses_post; still insert via DOM, not eval.
var bodyWrap = document.createElement('div');
bodyWrap.className = 'fca-ad__house-body';
bodyWrap.innerHTML = h.body;
// Strip any leftover scripts / handlers if present.
Array.prototype.forEach.call(bodyWrap.querySelectorAll('script'), function (s) { s.remove(); });
Array.prototype.forEach.call(bodyWrap.querySelectorAll('*'), function (el) {
Array.prototype.slice.call(el.attributes || []).forEach(function (a) {
if (/^on/i.test(a.name)) { el.removeAttribute(a.name); }
if (/^(href|src|action|formaction)$/i.test(a.name) && /^\s*(javascript|vbscript|data)\s*:/i.test(a.value || '')) {
el.setAttribute(a.name, '#');
}
});
});
inner += bodyWrap.outerHTML;
}
link.innerHTML = inner;
link.addEventListener('click', function () {
trackClick(ad.id, serveCfg);
});
body.appendChild(link);
return true;
}function parseSizes(str) {
if (!str) { return []; }
return String(str).split(',').map(function (s) {
var m = s.trim().match(/^(\d+)\s*x\s*(\d+)$/i);
return m ? [parseInt(m[1], 10), parseInt(m[2], 10)] : null;
}).filter(Boolean);
}function zoneCfgOf(res) {
return (res && res.zone) || { mode: 'manual', perRow: 1, count: 1, shown: 1, display: 'inherit', rotateSeconds: 8, gapMinutes: 0, frequency: 0, repeat: false };
}
// How many ads render side-by-side at once (grid columns). Prefers
// the new perRow field, falling back to the legacy `count`.
function perRowOf(zcfg) {
return Math.max(1, (zcfg && (zcfg.perRow || zcfg.count)) || 1);
}// Activate an ad into an already-mounted shell and (unless it's a
// synthetic demo/auto slot with no real id) wire impression tracking.
function activateInto(ad, shell, serveCfg) {
var kind = (shell.wrap && shell.wrap.getAttribute) ? shell.wrap.getAttribute(TAG) : '';
var ok = activate(ad, shell.body, serveCfg, kind);
if (!ok) { return false; }
if (!ad.demo && ad.id) {
shell.wrap.setAttribute('data-ad-id', ad.id);
shell.wrap.__fcaSeen = false;
watchImpression(shell.wrap, serveCfg);
}
return true;
}// Timed rotation: cycle the shell through the returned pool. Each
// step waits for the CURRENTLY shown ad's own duration (paid ads
// stick for their plan window), stepping by `perRow` so stacked
// slots stay distinct. Uses a self-rescheduling timeout so each ad
// can hold for a different length of time.
function setupRotation(shell, pool, startIdx, zcfg, serveCfg) {
if (!pool || pool.length < 2) { return; }
var step = perRowOf(zcfg);
var cur = startIdx;
window.__fcaAdTimers = window.__fcaAdTimers || [];
function durOf(ad) {
return Math.max(3, (ad && ad.durationSeconds) || zcfg.rotateSeconds || 8);
}
function schedule() {
var timer = setTimeout(function () {
if (!document.body.contains(shell.wrap)) { return; }
cur = (cur + step) % pool.length;
shell.body.innerHTML = '';
activateInto(pool[cur], shell, serveCfg);
schedule();
}, durOf(pool[cur]) * 1000);
window.__fcaAdTimers.push(timer);
}
schedule();
}// The effective display for a slot: each ad carries its own display
// (owner repeater sets it per entry); fall back to the zone config.
function displayOf(ad, zcfg) {
return (ad && ad.display) || (zcfg && zcfg.display) || 'inherit';
}
function rotateSecsOf(ad, zcfg) {
return (ad && ad.rotateSeconds) || (zcfg && zcfg.rotateSeconds) || 8;
}
// Any ad in the pool asking to rotate?
function anyRotate(list, zcfg) {
for (var i = 0; i < list.length; i++) {
if (displayOf(list[i], zcfg) === 'rotate') { return true; }
}
return false;
}// Place one slot from a pool at index `idx`; mountFn attaches the
// wrapper to the DOM. Rotation is decided by the *ad's* own display.
function placeOne(kind, pool, idx, zcfg, serveCfg, mountFn) {
var ad = pool[idx % pool.length];
var shell = buildShell(kind, ad, serveCfg);
mountFn(shell.wrap, ad);
if (!shell.wrap.parentNode) { return; }
if (!activateInto(ad, shell, serveCfg)) { shell.wrap.remove(); return; }
if (displayOf(ad, zcfg) === 'rotate') {
setupRotation(shell, pool, idx, {
rotateSeconds: rotateSecsOf(ad, zcfg),
perRow: perRowOf(zcfg),
}, serveCfg);
}
}// How many slots to place for a zone: `perRow` (simultaneous ads),
// capped by pool size unless any ad rotates (then each slot cycles
// the whole pool).
function slotCount(list, zcfg) {
var slots = perRowOf(zcfg);
return anyRotate(list, zcfg) ? slots : Math.min(slots, list.length);
}// Stack N slots into a container via a sequential mount callback.
// With more than one slot the slots are wrapped in a CSS grid whose
// column count matches the zone's `count`, so a placement showing
// e.g. 3 ads renders as a 3-up grid.
function placeStack(kind, res, mountFn) {
var list = ads(res);
if (!list.length) { return; }
var zcfg = zoneCfgOf(res);
var serveCfg = res.config || {};
var n = slotCount(list, zcfg);
if (n <= 1) {
placeOne(kind, list, 0, zcfg, serveCfg, mountFn);
return;
}
var grid = document.createElement('div');
grid.className = 'fca-zone-grid-slots';
grid.setAttribute(TAG, '1');
grid.style.setProperty('--fca-cols', String(Math.min(n, 6)));
mountFn(grid, list[0]);
if (!grid.parentNode) { return; }
for (var i = 0; i < n; i++) {
placeOne(kind, list, i, zcfg, serveCfg, function (el) { grid.appendChild(el); });
}
if (!grid.children.length) { grid.remove(); }
}// ── DOM targets ────────────────────────────────────────────
// The feed wrapper. Both the home feed (route `all_feeds`) and a
// space feed (route `space_feeds`) render the SAME component:
// `.fcom_feed_style_.all_feeds`. They are told apart by the
// page (onSpacePage) → global-feed vs space-feed zones, NOT by DOM.
function feedContainer() {
return document.querySelector('.fcom_feed_style_timeline.all_feeds')
|| document.querySelector('[class*="fcom_feed_style_"]');
}
// The element that directly holds the post items.
function feedHolder(container) {
if (!container) { return null; }
return container.querySelector('.all_feeds_holder') || container;
}
// Feed post items: FC wraps each post in `.feed_outer` inside
// `.all_feeds_holder`. Fall back to older/grid layouts.
function feedPosts(holder) {
if (!holder) { return []; }
var items = holder.querySelectorAll(':scope > .feed_outer');
if (items.length) { return items; }
items = holder.querySelectorAll(':scope > .feed, :scope > .fcom_feed_card, :scope > .fcom_feed_list, :scope > [id^="feed_id_"]');
if (items.length) { return items; }
return holder.querySelectorAll('.feed_outer, .feed, .fcom_feed_card, .fcom_feed_list');
}
function sidebar() {
return document.querySelector('.fcom_main_side_wrap')
|| document.querySelector('aside[role="complementary"] .fcom_main_side_wrap')
|| document.querySelector('#fluent_community_sidebar_menu .fcom_main_side_wrap')
|| document.querySelector('aside[role="complementary"]');
}
// Left navigation column (FC "space_contents" menu).
function leftSidebar() {
return document.querySelector('#fluent_community_sidebar_menu')
|| document.querySelector('.fcom_left_side')
|| document.querySelector('.spaces .space_contents');
}
// Main content column (used for single-post / event placements).
function mainContent() {
return document.querySelector('.feeds_main')
|| document.querySelector('.fcom_main')
|| document.querySelector('.fcom_content');
}
// Top of the main feed column — where the "header" (leaderboard)
// banner goes. Present on the portal home feed and space feeds.
// (FC's before_contents hook can't be used: it only renders inside
// the RecentActivities widget, i.e. the right sidebar.)
function feedColumn() {
return document.querySelector('.fcom_feed_box')
|| document.querySelector('.feeds_main .fcom_feed_box')
|| document.querySelector('.feeds_main .all_feeds')
|| document.querySelector('.feeds_main')
|| feedContainer();
}
var PATH = function () { return window.location.pathname + window.location.hash; };
// The FC portal admin area ({portalBase}/admin/…) is a
// management surface — never inject ads there.
function isAdminArea() { return /\/admin(\/|$)/.test(PATH()) || /#\/?admin(\/|$)/.test(PATH()); }
function onEventSingle() { return /\/event\/[^\/?#]+/.test(PATH()); }
function onEventList() { return /\/events(\/|\b)/.test(PATH()) && !onEventSingle(); }
// A single FluentCommunity feed post ("space post", NOT a WP
// post_type=post). FC uses /post/{slug} in the URL for the
// dedicated single-post page.
function onSinglePost() { return /\/post\/[^\/?#]+/.test(PATH()); }
// The single feed-post element, for BOTH the dedicated single-post
// page and the overlay popup (an el-dialog `.fcom_feed_modal`
// teleported to). FC marks the single/detail view with the
// `.feed_single` class (the timeline cards do NOT have it), so this
// one selector distinguishes it from the feed behind an open popup.
// Legacy class names kept as a fallback for older FC builds.
function singlePostCard() {
return document.querySelector('.fcom_feed_modal .feed_single')
|| document.querySelector('.feed_single')
|| document.querySelector('.fcom_single_layout .fcom_feed_card')
|| document.querySelector('.feeds_main .fcom_feed_card')
|| null;
}
// The post CONTENT body — where "inside" ads interleave and the
// anchor for before/after "content" placements.
function postContentBody(card) {
return card.querySelector('.feed_body')
|| card.querySelector('.fcom_feed_card_body')
|| card;
}
// The rendered markdown region for "inside content" interleaving.
function insideTarget(card) {
return card.querySelector('.feed_md_content')
|| card.querySelector('.feed_body')
|| card.querySelector('.fcom_feed_excerpt')
|| card.querySelector('.fcom_feed_data')
|| card.querySelector('.fcom_feed_card_body')
|| card;
}
// Insert an element at start|middle|end of a container's children.
function insertByPosition(container, el, position) {
var kids = Array.prototype.filter.call(container.children, function (n) {
return !n.hasAttribute || !n.hasAttribute(TAG);
});
if (position === 'start' || !kids.length) {
container.insertBefore(el, container.firstChild);
} else if (position === 'end') {
container.appendChild(el);
} else {
var mid = kids[Math.floor(kids.length / 2)];
container.insertBefore(el, mid);
}
}// Remove only JS-injected ads; server shells (data-fca-server) are
// owned by FC's feed rendering and clean themselves up on re-render.
function removeAds() {
if (window.__fcaAdTimers) {
window.__fcaAdTimers.forEach(function (t) { clearInterval(t); });
window.__fcaAdTimers = [];
}
document.querySelectorAll('[' + TAG + ']:not([data-fca-server])').forEach(function (n) { n.remove(); });
// Let single-post elements be re-decorated after a wipe.
document.querySelectorAll('.feed_single, .fcom_feed_card').forEach(function (el) {
el.__fcaSingleDone = false;
el.__fcaSinglePending = false;
});
// Let feed posts be re-decorated after a wipe.
document.querySelectorAll('.feed_outer, .feed, .fcom_feed_card, .fcom_feed_list, [id^="feed_id_"]').forEach(function (el) {
el.__fcaFeedDone = false;
});
state.lastFeed = null;
// Restart the feed rotation index for the new page/route so the
// top of the feed begins at the start of the pool again.
state.feedIdx = 0;
}// Place "inside content" slots. With more than one slot the ads are
// SPREAD across the content's block children (paragraphs, lists,
// etc.) instead of stacking at one spot — e.g. 3 inside ads land at
// ~1/4, ~2/4 and ~3/4 through the post body. A single slot honors
// the ad's own insidePosition (start/middle/end).
function placeInside(kind, res, content) {
var list = ads(res);
if (!list.length || !content) { return; }
var zcfg = zoneCfgOf(res);
var serveCfg = res.config || {};
var n = slotCount(list, zcfg);// Block-level children to interleave between (skip our own ads).
var kids = Array.prototype.filter.call(content.children, function (node) {
return !(node.hasAttribute && node.hasAttribute(TAG));
});if (n <= 1 || kids.length === 0) {
placeOne(kind, list, 0, zcfg, serveCfg, function (el, ad) {
insertByPosition(content, el, (ad && ad.insidePosition) || 'middle');
});
return;
}// Pick N anchor nodes up front (node refs stay valid as we
// insert new siblings), then drop each ad AFTER its anchor.
var total = kids.length;
var anchors = [];
for (var k = 0; k < n; k++) {
var idx = (n >= total)
? Math.min(k, total - 1)
: Math.floor((k + 1) * total / (n + 1));
if (idx > total - 1) { idx = total - 1; }
anchors.push(kids[idx]);
}
for (var j = 0; j < n; j++) {
(function (anchor, idx) {
placeOne(kind, list, idx, zcfg, serveCfg, function (el) {
anchor.parentNode.insertBefore(el, anchor.nextSibling);
});
})(anchors[j], j);
}
}// ── injectors ──────────────────────────────────────────────
// Insert ONE ad block right after `anchor`. The block shows up to
// `perBlock` ads (max 3) as a CSS grid — 1 ad = 1 column, 2 = 2,
// 3 = 3 — cycling the pool starting at `startIdx`.
function insertFeedBlock(list, startIdx, perBlock, zcfg, serveCfg, anchor) {
if (!anchor || !anchor.parentNode) { return; }
var cols = Math.min(3, Math.max(1, perBlock));
var slots = anyRotate(list, zcfg) ? cols : Math.min(cols, list.length);
if (slots < 1) { return; }
if (slots === 1) {
placeOne('feed', list, startIdx, zcfg, serveCfg, function (el) {
anchor.parentNode.insertBefore(el, anchor.nextSibling);
});
return;
}
var grid = document.createElement('div');
grid.className = 'fca-zone-grid-slots fca-feed-grid';
grid.setAttribute(TAG, '1');
grid.style.setProperty('--fca-cols', String(slots));
anchor.parentNode.insertBefore(grid, anchor.nextSibling);
for (var s = 0; s < slots; s++) {
placeOne('feed', list, startIdx + s, zcfg, serveCfg, function (el) { grid.appendChild(el); });
}
if (!grid.children.length) { grid.remove(); }
}// Between-feed / space-feed ads. Injects a grid block after every
// `frequency` posts. When `repeat` is on the block repeats down the
// whole feed (including posts loaded later via infinite scroll,
// caught by the feed observer); otherwise it appears once. Each
// block shows `count` ads (max 3) in a grid. Idempotent: each
// anchor post is decorated at most once.
function injectFeed(res) {
var container = feedContainer();
if (!container) { return; }
var list = ads(res);
if (!list.length) { return; }
var holder = feedHolder(container);
var posts = feedPosts(holder);
if (!posts.length) { return; }
var zcfg = zoneCfgOf(res);
var serveCfg = res.config || {};
var last = posts.length - 1;// Demo placeholders (no real ad yet) flow through the SAME
// frequency/repeat logic below so the owner previews the true
// "every N posts" + "repeat down feed" behavior, not a one-off.var every = Math.max(1, parseInt(zcfg.frequency, 10) || serveCfg.feedFrequency || 5);
var perBlock = Math.min(3, perRowOf(zcfg));
var repeat = !!zcfg.repeat;// Short feeds: clamp frequency so ads still appear (otherwise
// frequency=5 on a 3-post feed never injects).
if (posts.length > 1 && every >= posts.length) {
every = Math.max(1, posts.length - 1);
}for (var i = every - 1; i < posts.length; i += every) {
// Prefer not to decorate the final `.feed_outer`: FC uses
// `.all_feeds_holder .feed_outer:last-child` as its
// infinite-scroll sentinel. When the eligible slot *is*
// the last post (common with short feeds / frequency=5),
// insert after the previous post instead so global-feed
// ads still appear.
var anchorIdx = i;
if (anchorIdx >= last) {
if (last < 1) { break; }
anchorIdx = last - 1;
}
var anchor = posts[anchorIdx];
if (!anchor.__fcaFeedDone) {
// Continuous index across blocks AND across infinite
// scroll: each new block starts where the last left off
// and wraps the pool, so different ads appear as you
// scroll instead of repeating the first ones.
insertFeedBlock(list, state.feedIdx, perBlock, zcfg, serveCfg, anchor);
anchor.__fcaFeedDone = true;
state.feedIdx += perBlock;
}
if (!repeat) { break; }
// After falling back to last-1, further steps would re-use
// the same sentinel-safe anchor — stop.
if (i >= last) { break; }
}
// Remember the latest feed response so the observer can inject
// into posts appended by infinite scroll without a re-fetch.
state.lastFeed = res;
}function injectSidebar(res) {
var wrap = sidebar();
if (!wrap || !ads(res).length) { return; }
var anchor = wrap.querySelector('.app_side_widget') || wrap.firstElementChild;
placeStack('sidebar', res, function (el) {
if (anchor && anchor.parentNode) {
anchor.parentNode.insertBefore(el, anchor.nextSibling);
anchor = el;
} else {
wrap.appendChild(el);
anchor = el;
}
});
}function injectLeftSidebar(res) {
var wrap = leftSidebar();
if (!wrap || !ads(res).length) { return; }
placeStack('sidebar-left', res, function (el) {
wrap.appendChild(el);
});
}// Header (top banner / leaderboard): placed at the very top of the
// main feed column, above the post composer and the feed list.
function injectHeader(res) {
var col = feedColumn();
if (!col || !ads(res).length) { return; }
// Avoid stacking duplicate headers if a prior run left one
// (SPA re-renders sometimes keep server shells).
if (col.querySelector('[' + TAG + '="header"]')) { return; }
placeStack('header', res, function (el) {
col.insertBefore(el, col.firstChild);
});
}// Single-post before / inside / after placements. "before/after
// content" anchor around the post body (`.feed_body`), so the ads
// sit right above / below the content and don't land after the
// comment thread inside the popup.
function injectSinglePost(before, inside, after, card) {
card = card || singlePostCard();
if (!card) { return; }
var body = postContentBody(card);
placeStack('post-before', before, function (el) {
body.parentNode.insertBefore(el, body);
});
placeInside('post-inside', inside, insideTarget(card));
var afterAnchor = body;
placeStack('post-after', after, function (el) {
afterAnchor.parentNode.insertBefore(el, afterAnchor.nextSibling);
afterAnchor = el;
});
}// Inject single-post ads once per post element. Works for the
// dedicated page AND the overlay popup (which may open with NO route
// change, so a MutationObserver — not just route hooks — drives it).
// A per-element flag prevents duplicate fetches while the popup or
// page re-renders (comments, reactions, etc.).
function syncSinglePost() {
if (isAdminArea()) { return; }
var el = singlePostCard();
if (!el || el.__fcaSingleDone || el.__fcaSinglePending) { return; }
el.__fcaSinglePending = true;
Promise.all([
fetchServe('post-before', 10),
fetchServe('post-inside', 10),
fetchServe('post-after', 10)
]).then(function (a) {
if (singlePostCard() !== el || !document.body.contains(el)) {
el.__fcaSinglePending = false;
return;
}
injectSinglePost(a[0], a[1], a[2], el);
el.__fcaSingleDone = true;
el.__fcaSinglePending = false;
}).catch(function () { el.__fcaSinglePending = false; });
}// Event single (before/inside/after) and event listing placements.
function injectEvents(bundle) {
var main = mainContent();
if (!main) { return; }
if (onEventSingle()) {
var beforeAnchor = main.firstChild;
placeStack('event-single-before', bundle.before, function (el) {
main.insertBefore(el, beforeAnchor);
});
placeInside('event-single-inside', bundle.inside, main);
placeStack('event-single-after', bundle.after, function (el) {
main.appendChild(el);
});
} else if (onEventList()) {
var listAnchor = main.firstChild;
placeStack('event-list', bundle.list, function (el) {
main.insertBefore(el, listAnchor);
});
}
}// ── server-rendered ad shells (e.g. the [fca_ad] shortcode) ──
// Activate + track any shell rendered into the DOM server-side.
function activateServerAds(serveCfg) {
var shells = document.querySelectorAll('[data-fca-server="1"]:not([data-fca-activated])');
shells.forEach(function (shell) {
shell.setAttribute('data-fca-activated', '1');
var body = shell.querySelector('.fca-ad__body') || shell;
var type = shell.getAttribute('data-ad-type');
var adId = shell.getAttribute('data-ad-id');// Demo shells already contain their placeholder markup;
// just track the impression, never call the network.
if (shell.getAttribute('data-fca-demo') === '1') {
watchImpression(shell, serveCfg);
return;
}addReportControl(shell, { id: adId, demo: false }, serveCfg);if (type === 'adsense') {
loadAdSense(shell.getAttribute('data-ad-client'));
try { (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) {}
} else if (type === 'gam') {
loadGpt();
var sizes = parseSizes(shell.getAttribute('data-gam-sizes'));
var unit = shell.getAttribute('data-gam-unit');
var divId = 'fca-gpt-' + adId + '-' + Math.random().toString(36).slice(2, 8);
var slotDiv = document.createElement('div');
slotDiv.id = divId;
body.appendChild(slotDiv);
window.googletag = window.googletag || { cmd: [] };
window.googletag.cmd.push(function () {
var slot = window.googletag.defineSlot(unit, sizes.length ? sizes : ['fluid'], divId);
if (slot) {
slot.addService(window.googletag.pubads());
window.googletag.enableServices();
window.googletag.display(divId);
}
});
} else if (type === 'custom' || shell.getAttribute('data-fca-custom') === '1') {
// Ensure fixed size + sandboxed iframe (migrates older
// raw-HTML shells that predate the sandbox).
var kind = shell.getAttribute(TAG) || 'feed';
var sizeKey = shell.getAttribute('data-fca-size') || '';
var size = sizeForKind(kind, sizeKey ? { sizeKey: sizeKey } : null);
// Prefer dimensions already stamped on the server shell.
var dw = parseInt(shell.getAttribute('data-fca-w') || '0', 10);
var dh = parseInt(shell.getAttribute('data-fca-h') || '0', 10);
if (dw > 0 && dh > 0) {
size = { w: dw, h: dh, key: sizeKey || size.key, label: dw + '\u00d7' + dh };
}
applyFixedSize(body, size);
shell.classList.add('fca-ad--fixed');
shell.setAttribute('data-fca-w', String(size.w || 336));
shell.setAttribute('data-fca-h', String(size.h || 280));
if (!body.querySelector('iframe.fca-ad__sandbox')) {
var html = body.innerHTML;
body.innerHTML = '';
renderSandboxed(body, html, size, adId);
} else {
var frame = body.querySelector('iframe.fca-ad__sandbox');
if (frame) {
frame.style.height = (size.h || 280) + 'px';
frame.height = String(size.h || 280);
if (adId) { frame.setAttribute('data-ad-id', String(adId)); }
// Re-clip / inject click beacon for older sandboxed creatives.
try {
var doc = frame.getAttribute('srcdoc') || '';
if (doc && doc.indexOf('fca-ad-click') === -1) {
if (doc.indexOf('fca-ad-clip') === -1) {
frame.srcdoc = clippedSrcdoc(doc, size, adId);
} else if (adId) {
var beaconOnly = '')
: (doc + beaconOnly);
}
}
} catch (e) {}
}
}
} else {
var link = shell.querySelector('.fca-ad__house');
if (link) {
link.addEventListener('click', function () { trackClick(adId, serveCfg); });
}
}
watchImpression(shell, serveCfg);
});
}// ── run ────────────────────────────────────────────────────
function ads(res) { return (res && res.ads) || []; }function run() {
var gen = ++state.gen;
removeAds();
// Suppress all ads in the portal admin area.
if (isAdminArea()) { return; }var feedZone = onSpacePage() ? 'space-feed' : 'global-feed';
var eventSingle = onEventSingle();
var eventList = onEventList();// Single feed-post placements (dedicated page OR overlay popup).
// Driven by syncSinglePost() which is idempotent per element;
// the overlay observer also calls it for popups that open with
// no route change.
syncSinglePost();// Zones fetched every run (cheap, cached server-side). Limit
// 10 = the max per-zone `count`; the server clamps as needed.
// Always request header — injectHeader no-ops until the feed
// column exists (SPA may mount it slightly later).
var jobs = {
feed: fetchServe(feedZone, 10),
sidebar: fetchServe('sidebar', 10),
left: fetchServe('sidebar-left', 10),
header: fetchServe('header', 10)
};
if (eventSingle) {
jobs.eBefore = fetchServe('event-single-before', 10);
jobs.eInside = fetchServe('event-single-inside', 10);
jobs.eAfter = fetchServe('event-single-after', 10);
} else if (eventList) {
jobs.eList = fetchServe('event-list', 10);
}var keys = Object.keys(jobs);
Promise.all(keys.map(function (k) { return jobs[k]; })).then(function (arr) {
if (gen !== state.gen) { return; }
var r = {};
keys.forEach(function (k, i) { r[k] = arr[i]; });
var serveCfg = (r.feed && r.feed.config) || (r.sidebar && r.sidebar.config) || {};
if (serveCfg.zoneSizes) {
CFG.zoneSizes = serveCfg.zoneSizes;
}
if (serveCfg.zoneOptions) {
CFG.zoneOptions = serveCfg.zoneOptions;
}activateServerAds(serveCfg);
injectHeader(r.header);
injectFeed(r.feed);
injectSidebar(r.sidebar);
injectLeftSidebar(r.left);
if (eventSingle || eventList) {
injectEvents({
before: r.eBefore, inside: r.eInside,
after: r.eAfter, list: r.eList
});
}
});
}function waitForFeed(fn, n) {
n = n || 0;
if (feedContainer() || sidebar() || leftSidebar() || mainContent()) { fn(); return; }
if (n >= 60) { return; }
setTimeout(function () { waitForFeed(fn, n + 1); }, 100);
}function debounce(fn, ms) {
var t; return function () { clearTimeout(t); t = setTimeout(fn, ms); };
}
var onNav = debounce(function () { waitForFeed(run); }, 150);// history patch for SPA nav
if (!window.__fcaAdsHistoryPatched) {
window.__fcaAdsHistoryPatched = true;
var op = history.pushState, orp = history.replaceState;
history.pushState = function () { var r = op.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); return r; };
history.replaceState = function () { var r = orp.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); return r; };
}
window.addEventListener('popstate', onNav);
window.addEventListener('hashchange', onNav);
window.addEventListener('locationchange', onNav);
document.addEventListener('fluentComPortalAppReady', onNav);// The overlay popup (el-dialog `.fcom_feed_modal`) is teleported to
// and can open WITHOUT a route change, so route hooks alone
// miss it. Watch the DOM and decorate the single-post element when
// it appears; syncSinglePost() is idempotent per element (guarded by
// a flag) so our own insertions never trigger a re-inject loop.
// Infinite scroll appends posts without a route change. Re-run the
// (idempotent) feed injector against the last feed response so new
// posts get their ad blocks too. No-op when nothing new appeared.
function syncFeed() {
if (isAdminArea() || !state.lastFeed) { return; }
injectFeed(state.lastFeed);
}if ('MutationObserver' in window) {
var onBodyMutation = debounce(function () { syncSinglePost(); syncFeed(); }, 120);
var singleMo = new MutationObserver(onBodyMutation);
var startSingleObserver = function () {
if (document.body) { singleMo.observe(document.body, { childList: true, subtree: true }); }
};
if (document.body) { startSingleObserver(); }
else { document.addEventListener('DOMContentLoaded', startSingleObserver); }
}function registerFcHooks() {
var util = window.FluentCommunityUtil;
if (!util || !util.hooks || typeof util.hooks.addAction !== 'function') { return false; }
util.hooks.addAction('fluent_com_route_changed', 'fca_ads_injector_nav', onNav);
return true;
}
if (!registerFcHooks()) {
document.addEventListener('fluentCommunityUtilReady', function () { registerFcHooks(); onNav(); });
}if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { waitForFeed(run); });
} else {
waitForFeed(run);
}
})();