nodebb-plugin-ezoic-infinite 1.5.20 → 1.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/library.js +44 -10
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/public/admin.js +2 -3
- package/public/client.js +672 -196
- package/public/style.css +15 -5
package/library.js
CHANGED
|
@@ -9,7 +9,11 @@ const plugin = {};
|
|
|
9
9
|
|
|
10
10
|
function normalizeExcludedGroups(value) {
|
|
11
11
|
if (!value) return [];
|
|
12
|
-
|
|
12
|
+
// NodeBB settings may return arrays, strings, or objects like {0:'a',1:'b'}
|
|
13
|
+
if (Array.isArray(value)) return value.map(String).map(s => s.trim()).filter(Boolean);
|
|
14
|
+
if (typeof value === 'object') {
|
|
15
|
+
return Object.values(value).map(String).map(s => s.trim()).filter(Boolean);
|
|
16
|
+
}
|
|
13
17
|
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -27,13 +31,21 @@ async function getAllGroups() {
|
|
|
27
31
|
}
|
|
28
32
|
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
29
33
|
const data = await groups.getGroupsData(filtered);
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
|
|
35
|
+
const valid = data.filter(g => g && g.name);
|
|
36
|
+
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
37
|
+
return valid;
|
|
33
38
|
}
|
|
39
|
+
let _settingsCache = null;
|
|
40
|
+
let _settingsCacheAt = 0;
|
|
41
|
+
const SETTINGS_TTL = 30000; // 30s
|
|
42
|
+
|
|
34
43
|
async function getSettings() {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
35
46
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
36
|
-
|
|
47
|
+
_settingsCacheAt = Date.now();
|
|
48
|
+
_settingsCache = {
|
|
37
49
|
// Between-post ads (simple blocks) in category topic list
|
|
38
50
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
39
51
|
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
@@ -54,14 +66,32 @@ async function getSettings() {
|
|
|
54
66
|
|
|
55
67
|
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
56
68
|
};
|
|
69
|
+
return _settingsCache;
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
async function isUserExcluded(uid, excludedGroups) {
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
73
|
+
const list = (excludedGroups || []).map(g => String(g).toLowerCase());
|
|
74
|
+
const id = Number(uid) || 0;
|
|
75
|
+
|
|
76
|
+
if (!list.length) return false;
|
|
77
|
+
|
|
78
|
+
// Guests (uid=0) are not in groups.getUserGroups; treat explicitly if configured
|
|
79
|
+
if (id === 0) {
|
|
80
|
+
return list.includes('guests') || list.includes('guest');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const userGroups = await groups.getUserGroups([id]);
|
|
84
|
+
const names = (userGroups[0] || []).map(g => (g && g.name) ? g.name : String(g));
|
|
85
|
+
return names.some(name => list.includes(String(name).toLowerCase()));
|
|
63
86
|
}
|
|
64
87
|
|
|
88
|
+
plugin.onSettingsSet = function (data) {
|
|
89
|
+
// Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
|
|
90
|
+
if (data && data.hash === SETTINGS_KEY) {
|
|
91
|
+
_settingsCache = null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
65
95
|
plugin.addAdminNavigation = async (header) => {
|
|
66
96
|
header.plugins = header.plugins || [];
|
|
67
97
|
header.plugins.push({
|
|
@@ -89,9 +119,13 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
89
119
|
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
90
120
|
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
91
121
|
|
|
92
|
-
router.get('/api/plugins/ezoic-infinite/config',
|
|
122
|
+
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
93
123
|
const settings = await getSettings();
|
|
94
|
-
const
|
|
124
|
+
const uid = (typeof req.uid !== 'undefined' && req.uid !== null) ? req.uid
|
|
125
|
+
: (req.user && req.user.uid) ? req.user.uid
|
|
126
|
+
: (res.locals && res.locals.uid) ? res.locals.uid
|
|
127
|
+
: 0;
|
|
128
|
+
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
95
129
|
|
|
96
130
|
res.json({
|
|
97
131
|
excluded,
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
e.preventDefault();
|
|
14
14
|
|
|
15
15
|
Settings.save('ezoic-infinite', $form, function () {
|
|
16
|
-
// Toast vert (NodeBB core)
|
|
17
16
|
if (alerts && typeof alerts.success === 'function') {
|
|
18
|
-
alerts.success('
|
|
17
|
+
alerts.success('[[admin/settings:saved]]');
|
|
19
18
|
} else if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
20
|
-
window.app.alertSuccess('
|
|
19
|
+
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
21
20
|
}
|
|
22
21
|
});
|
|
23
22
|
});
|
package/public/client.js
CHANGED
|
@@ -1,45 +1,93 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
var $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
var SELECTORS = {
|
|
7
7
|
topicItem: 'li[component="category/topic"]',
|
|
8
8
|
postItem: '[component="post"][data-pid]',
|
|
9
|
-
categoryItem: 'li[component="categories/category"]'
|
|
9
|
+
categoryItem: 'li[component="categories/category"]'
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const MAX_INSERTS_PER_RUN = 3;
|
|
12
|
+
var WRAP_CLASS = 'ezoic-ad';
|
|
13
|
+
var PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
// Hard limits to avoid runaway insertion during mutations / infinite scroll.
|
|
16
|
+
var MAX_INSERTS_PER_RUN = 3;
|
|
17
|
+
|
|
18
|
+
// Placeholders that have been "defined" (filled) at least once in this browser session.
|
|
19
|
+
// This survives ajaxify navigations, which is important for safe recycle/destroy logic.
|
|
20
|
+
var sessionDefinedIds = new Set();
|
|
21
|
+
|
|
22
|
+
// Prevent re-entrant insertion of the same id while Ezoic is processing it.
|
|
23
|
+
var insertingIds = new Set();
|
|
24
|
+
|
|
25
|
+
var state = {
|
|
17
26
|
pageKey: null,
|
|
27
|
+
|
|
18
28
|
cfg: null,
|
|
19
29
|
cfgPromise: null,
|
|
30
|
+
|
|
20
31
|
poolTopics: [],
|
|
21
32
|
poolPosts: [],
|
|
22
33
|
poolCategories: [],
|
|
34
|
+
|
|
23
35
|
usedTopics: new Set(),
|
|
24
36
|
usedPosts: new Set(),
|
|
25
37
|
usedCategories: new Set(),
|
|
38
|
+
|
|
39
|
+
// Track wrappers that are still in the DOM to recycle ids once they are far above viewport.
|
|
40
|
+
liveTopics: [],
|
|
41
|
+
livePosts: [],
|
|
42
|
+
liveCategories: [],
|
|
43
|
+
|
|
44
|
+
// Throttle showAds calls per id.
|
|
26
45
|
lastShowById: new Map(),
|
|
27
|
-
|
|
46
|
+
|
|
47
|
+
// Ids for which we scheduled/attempted showAds and should not schedule again immediately.
|
|
48
|
+
pendingById: new Set(),
|
|
49
|
+
|
|
50
|
+
// Timeouts created by this script (so we can cancel on ajaxify.start).
|
|
51
|
+
activeTimeouts: new Set(),
|
|
52
|
+
|
|
53
|
+
// Run scheduling / mutation observer.
|
|
28
54
|
scheduled: false,
|
|
29
55
|
timer: null,
|
|
30
56
|
obs: null,
|
|
31
|
-
};
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
// Scroll throttling.
|
|
59
|
+
lastScrollRun: 0,
|
|
60
|
+
|
|
61
|
+
// Navigation safety gate: we only insert after ajaxify.end settles.
|
|
62
|
+
canShowAds: false,
|
|
63
|
+
|
|
64
|
+
// Retry counters.
|
|
65
|
+
poolWaitAttempts: 0,
|
|
66
|
+
awaitItemsAttempts: 0
|
|
67
|
+
};
|
|
34
68
|
|
|
35
69
|
function normalizeBool(v) {
|
|
36
70
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
37
71
|
}
|
|
38
72
|
|
|
73
|
+
function setTimeoutTracked(fn, ms) {
|
|
74
|
+
var id = setTimeout(fn, ms);
|
|
75
|
+
state.activeTimeouts.add(id);
|
|
76
|
+
return id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clearAllTrackedTimeouts() {
|
|
80
|
+
state.activeTimeouts.forEach(function (id) {
|
|
81
|
+
try { clearTimeout(id); } catch (e) {}
|
|
82
|
+
});
|
|
83
|
+
state.activeTimeouts.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
39
86
|
function uniqInts(lines) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
var out = [];
|
|
88
|
+
var seen = new Set();
|
|
89
|
+
for (var i = 0; i < lines.length; i++) {
|
|
90
|
+
var n = parseInt(lines[i], 10);
|
|
43
91
|
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
44
92
|
seen.add(n);
|
|
45
93
|
out.push(n);
|
|
@@ -50,26 +98,34 @@
|
|
|
50
98
|
|
|
51
99
|
function parsePool(raw) {
|
|
52
100
|
if (!raw) return [];
|
|
53
|
-
|
|
101
|
+
var lines = String(raw)
|
|
102
|
+
.split(/\r?\n/)
|
|
103
|
+
.map(function (s) { return s.trim(); })
|
|
104
|
+
.filter(Boolean);
|
|
54
105
|
return uniqInts(lines);
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
function getPageKey() {
|
|
58
109
|
try {
|
|
59
|
-
|
|
110
|
+
var ax = window.ajaxify;
|
|
60
111
|
if (ax && ax.data) {
|
|
61
|
-
if (ax.data.tid) return
|
|
62
|
-
if (ax.data.cid) return
|
|
112
|
+
if (ax.data.tid) return 'topic:' + ax.data.tid;
|
|
113
|
+
if (ax.data.cid) return 'cid:' + ax.data.cid + ':' + window.location.pathname;
|
|
63
114
|
}
|
|
64
115
|
} catch (e) {}
|
|
65
116
|
return window.location.pathname;
|
|
66
117
|
}
|
|
67
118
|
|
|
68
119
|
function getKind() {
|
|
69
|
-
|
|
120
|
+
var p = window.location.pathname || '';
|
|
70
121
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
71
122
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
72
123
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
124
|
+
|
|
125
|
+
// Fallback by DOM.
|
|
126
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
127
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
128
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
73
129
|
return 'other';
|
|
74
130
|
}
|
|
75
131
|
|
|
@@ -77,295 +133,715 @@
|
|
|
77
133
|
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
78
134
|
}
|
|
79
135
|
|
|
80
|
-
function getPostContainers() {
|
|
81
|
-
return Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
136
|
function getCategoryItems() {
|
|
85
137
|
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
86
138
|
}
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
function getPostContainers() {
|
|
141
|
+
var nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
142
|
+
return nodes.filter(function (el) {
|
|
143
|
+
if (!el || !el.isConnected) return false;
|
|
144
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
91
145
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const cfg = await res.json();
|
|
97
|
-
state.cfg = cfg;
|
|
98
|
-
return cfg;
|
|
99
|
-
} catch (e) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
})();
|
|
146
|
+
// Prevent nested / duplicated post wrappers.
|
|
147
|
+
var parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
148
|
+
if (parentPost && parentPost !== el) return false;
|
|
149
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
103
150
|
|
|
104
|
-
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
105
153
|
}
|
|
106
154
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
if (!state.poolPosts.length) state.poolPosts = parsePool(cfg.poolPosts);
|
|
110
|
-
if (!state.poolCategories.length) state.poolCategories = parsePool(cfg.poolCategories);
|
|
155
|
+
function safeRect(el) {
|
|
156
|
+
try { return el.getBoundingClientRect(); } catch (e) { return null; }
|
|
111
157
|
}
|
|
112
158
|
|
|
113
159
|
function destroyPlaceholderIds(ids) {
|
|
114
160
|
if (!ids || !ids.length) return;
|
|
115
|
-
|
|
161
|
+
|
|
162
|
+
// Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
|
|
163
|
+
var filtered = ids.filter(function (id) {
|
|
164
|
+
try { return sessionDefinedIds.has(id); } catch (e) { return true; }
|
|
165
|
+
});
|
|
166
|
+
|
|
116
167
|
if (!filtered.length) return;
|
|
117
168
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const call = () => {
|
|
123
|
-
if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
169
|
+
var call = function () {
|
|
170
|
+
try {
|
|
171
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
124
172
|
window.ezstandalone.destroyPlaceholders(filtered);
|
|
125
173
|
}
|
|
126
|
-
}
|
|
174
|
+
} catch (e) {}
|
|
175
|
+
};
|
|
127
176
|
|
|
177
|
+
try {
|
|
178
|
+
// Do NOT initialize ezstandalone here; if you load Ezoic elsewhere, it will manage its own queue.
|
|
128
179
|
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
129
180
|
call();
|
|
130
|
-
} else {
|
|
181
|
+
} else if (window.ezstandalone && window.ezstandalone.cmd && Array.isArray(window.ezstandalone.cmd)) {
|
|
131
182
|
window.ezstandalone.cmd.push(call);
|
|
132
183
|
}
|
|
184
|
+
} catch (e) {}}
|
|
185
|
+
|
|
186
|
+
function getRecyclable(liveArr) {
|
|
187
|
+
var margin = 600; // px above viewport
|
|
188
|
+
for (var i = 0; i < liveArr.length; i++) {
|
|
189
|
+
var entry = liveArr[i];
|
|
190
|
+
if (!entry || !entry.wrap || !entry.wrap.isConnected) {
|
|
191
|
+
liveArr.splice(i, 1);
|
|
192
|
+
i--;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
var r = safeRect(entry.wrap);
|
|
196
|
+
if (r && r.bottom < -margin) {
|
|
197
|
+
liveArr.splice(i, 1);
|
|
198
|
+
return entry;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
133
203
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
function pickId(pool, liveArr) {
|
|
205
|
+
if (pool.length) return { id: pool.shift(), recycled: null };
|
|
206
|
+
|
|
207
|
+
var recycled = getRecyclable(liveArr);
|
|
208
|
+
if (recycled) return { id: recycled.id, recycled: recycled };
|
|
209
|
+
|
|
210
|
+
return { id: null, recycled: null };
|
|
139
211
|
}
|
|
140
212
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
wrap.
|
|
144
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
145
|
-
wrap.style.width = '100%';
|
|
213
|
+
function resetPlaceholderInWrap(wrap, id) {
|
|
214
|
+
if (!wrap) return null;
|
|
215
|
+
try { wrap.innerHTML = ''; } catch (e) {}
|
|
146
216
|
|
|
147
|
-
|
|
148
|
-
ph.id =
|
|
217
|
+
var ph = document.createElement('div');
|
|
218
|
+
ph.id = PLACEHOLDER_PREFIX + id;
|
|
149
219
|
wrap.appendChild(ph);
|
|
220
|
+
return ph;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isAdjacentAd(el) {
|
|
224
|
+
var next = el && el.nextElementSibling;
|
|
225
|
+
return !!(next && next.classList && next.classList.contains(WRAP_CLASS));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isPrevAd(el) {
|
|
229
|
+
var prev = el && el.previousElementSibling;
|
|
230
|
+
return !!(prev && prev.classList && prev.classList.contains(WRAP_CLASS));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildWrap(id, kindClass) {
|
|
234
|
+
var wrap = document.createElement('div');
|
|
235
|
+
wrap.className = WRAP_CLASS + ' ' + kindClass;
|
|
236
|
+
wrap.setAttribute('data-ezoic-id', String(id));
|
|
237
|
+
resetPlaceholderInWrap(wrap, id);
|
|
150
238
|
return wrap;
|
|
151
239
|
}
|
|
152
240
|
|
|
153
241
|
function findWrap(kindClass, afterPos) {
|
|
154
|
-
|
|
242
|
+
// Search a wrapper marker that we set on insertion.
|
|
243
|
+
return document.querySelector('.' + kindClass + '[data-after-pos="' + afterPos + '"]');
|
|
155
244
|
}
|
|
156
245
|
|
|
157
|
-
function insertAfter(
|
|
158
|
-
if (!target || !target.insertAdjacentElement) return null;
|
|
159
|
-
if (findWrap(kindClass, afterPos)) return null;
|
|
160
|
-
|
|
246
|
+
function insertAfter(el, id, kindClass, afterPos) {
|
|
161
247
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
248
|
+
var wrap = buildWrap(id, kindClass);
|
|
249
|
+
wrap.setAttribute('data-after-pos', String(afterPos));
|
|
250
|
+
|
|
251
|
+
if (!el || !el.parentNode) return null;
|
|
252
|
+
if (el.nextSibling) el.parentNode.insertBefore(wrap, el.nextSibling);
|
|
253
|
+
else el.parentNode.appendChild(wrap);
|
|
254
|
+
|
|
255
|
+
attachFillObserver(wrap, id);
|
|
164
256
|
return wrap;
|
|
165
|
-
} catch (e) {
|
|
166
|
-
|
|
167
|
-
}
|
|
257
|
+
} catch (e) {}
|
|
258
|
+
return null;
|
|
168
259
|
}
|
|
169
260
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
261
|
+
function destroyUsedPlaceholders() {
|
|
262
|
+
var ids = [];
|
|
263
|
+
state.usedTopics.forEach(function (id) { ids.push(id); });
|
|
264
|
+
state.usedPosts.forEach(function (id) { ids.push(id); });
|
|
265
|
+
state.usedCategories.forEach(function (id) { ids.push(id); });
|
|
266
|
+
|
|
267
|
+
// Only destroy placeholders that were filled at least once in this session.
|
|
268
|
+
destroyPlaceholderIds(ids);
|
|
173
269
|
}
|
|
174
270
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
271
|
+
function patchShowAds() {
|
|
272
|
+
// Intentionally left blank: ezstandalone is managed elsewhere.
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function markFilled(id) {
|
|
276
|
+
try { sessionDefinedIds.add(id); } catch (e) {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isWrapMarkedFilled(wrap) {
|
|
280
|
+
try { return !!(wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'); } catch (e) { return false; }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function attachFillObserver(wrap, id) {
|
|
284
|
+
if (!wrap || !wrap.isConnected) return;
|
|
285
|
+
|
|
286
|
+
// If already filled, mark and return.
|
|
287
|
+
if (isPlaceholderFilled(wrap)) {
|
|
288
|
+
try { wrap.setAttribute('data-ezoic-filled', '1'); } catch (e) {}
|
|
289
|
+
markFilled(id);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Observe for Ezoic inserting ad content into placeholder.
|
|
178
294
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
295
|
+
var obs = new MutationObserver(function () {
|
|
296
|
+
if (!wrap.isConnected) {
|
|
297
|
+
try { obs.disconnect(); } catch (e) {}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (isPlaceholderFilled(wrap)) {
|
|
301
|
+
try { wrap.setAttribute('data-ezoic-filled', '1'); } catch (e) {}
|
|
302
|
+
markFilled(id);
|
|
303
|
+
try { obs.disconnect(); } catch (e) {}
|
|
186
304
|
}
|
|
187
305
|
});
|
|
306
|
+
obs.observe(wrap, { childList: true, subtree: true });
|
|
307
|
+
wrap.__ezoicFillObs = obs;
|
|
308
|
+
} catch (e) {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isPlaceholderFilled(wrap) {
|
|
312
|
+
// Heuristic: placeholder exists AND has descendants or meaningful height.
|
|
313
|
+
try {
|
|
314
|
+
var ph = wrap.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
315
|
+
if (!ph) return false;
|
|
316
|
+
if (ph.children && ph.children.length) return true;
|
|
317
|
+
var r = safeRect(wrap);
|
|
318
|
+
if (r && r.height > 20) return true;
|
|
188
319
|
} catch (e) {}
|
|
320
|
+
return false;
|
|
189
321
|
}
|
|
190
322
|
|
|
191
|
-
function
|
|
192
|
-
if (!
|
|
323
|
+
function scheduleShowAdsBatch(ids) {
|
|
324
|
+
if (!ids || !ids.length) return;
|
|
193
325
|
|
|
194
|
-
|
|
195
|
-
|
|
326
|
+
// Ezoic expects DOM to be settled.
|
|
327
|
+
var call = function () {
|
|
328
|
+
try {
|
|
329
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
330
|
+
window.ezstandalone.showAds(ids);
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {}
|
|
333
|
+
};
|
|
196
334
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
335
|
+
try {
|
|
336
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') call();
|
|
337
|
+
else if (window.ezstandalone && window.ezstandalone.cmd && Array.isArray(window.ezstandalone.cmd)) window.ezstandalone.cmd.push(call);
|
|
338
|
+
} catch (e) {}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function callShowAdsWhenReady(id) {
|
|
342
|
+
if (!id) return;
|
|
343
|
+
|
|
344
|
+
// Throttle per-id.
|
|
345
|
+
var now = Date.now();
|
|
346
|
+
var last = state.lastShowById.get(id) || 0;
|
|
347
|
+
if (now - last < 1200) return;
|
|
348
|
+
|
|
349
|
+
if (state.pendingById.has(id)) return;
|
|
350
|
+
state.pendingById.add(id);
|
|
351
|
+
state.lastShowById.set(id, now);
|
|
352
|
+
|
|
353
|
+
// Guard against re-entrancy.
|
|
354
|
+
if (insertingIds.has(id)) {
|
|
355
|
+
state.pendingById.delete(id);
|
|
356
|
+
return;
|
|
202
357
|
}
|
|
358
|
+
insertingIds.add(id);
|
|
359
|
+
|
|
360
|
+
var attempts = 0;
|
|
203
361
|
|
|
204
|
-
|
|
362
|
+
(function waitForPh() {
|
|
363
|
+
attempts++;
|
|
364
|
+
|
|
365
|
+
// Navigation safety: if we navigated away, stop.
|
|
366
|
+
if (!state.canShowAds) {
|
|
367
|
+
state.pendingById.delete(id);
|
|
368
|
+
insertingIds.delete(id);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
var ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
373
|
+
|
|
374
|
+
var doCall = function () {
|
|
375
|
+
try {
|
|
376
|
+
// If placeholder is gone, stop.
|
|
377
|
+
if (!ph || !ph.isConnected) return false;
|
|
378
|
+
scheduleShowAdsBatch([id]);
|
|
379
|
+
return true;
|
|
380
|
+
} catch (e) {}
|
|
381
|
+
return false;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (ph && ph.isConnected) {
|
|
385
|
+
doCall();
|
|
386
|
+
state.pendingById.delete(id);
|
|
387
|
+
insertingIds.delete(id);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (attempts < 100) {
|
|
392
|
+
setTimeoutTracked(waitForPh, 50);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Timeout: give up silently.
|
|
397
|
+
state.pendingById.delete(id);
|
|
398
|
+
insertingIds.delete(id);
|
|
399
|
+
})();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function initPools(cfg) {
|
|
403
|
+
if (!state.poolTopics.length) state.poolTopics = parsePool(cfg.placeholderIds);
|
|
404
|
+
if (!state.poolPosts.length) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
|
|
405
|
+
if (!state.poolCategories.length) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function computeTargets(count, interval, showFirst) {
|
|
409
|
+
var out = [];
|
|
410
|
+
if (count <= 0) return out;
|
|
411
|
+
|
|
412
|
+
if (showFirst) out.push(1);
|
|
413
|
+
|
|
414
|
+
for (var i = 1; i <= count; i++) {
|
|
415
|
+
if (i % interval === 0) out.push(i);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Unique + sorted.
|
|
419
|
+
return Array.from(new Set(out)).sort(function (a, b) { return a - b; });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet, liveArr) {
|
|
423
|
+
if (!items || !items.length) return 0;
|
|
424
|
+
|
|
425
|
+
var targets = computeTargets(items.length, interval, showFirst);
|
|
426
|
+
var inserted = 0;
|
|
427
|
+
|
|
428
|
+
for (var t = 0; t < targets.length; t++) {
|
|
429
|
+
var afterPos = targets[t];
|
|
205
430
|
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
206
431
|
|
|
207
|
-
|
|
432
|
+
var el = items[afterPos - 1];
|
|
208
433
|
if (!el || !el.isConnected) continue;
|
|
434
|
+
|
|
435
|
+
// Prevent adjacent ads.
|
|
436
|
+
if (isAdjacentAd(el) || isPrevAd(el)) continue;
|
|
437
|
+
|
|
438
|
+
// Prevent duplicates at same logical position.
|
|
439
|
+
if (findWrap(kindClass, afterPos - 1)) continue;
|
|
209
440
|
if (findWrap(kindClass, afterPos)) continue;
|
|
210
441
|
|
|
211
|
-
|
|
442
|
+
var pick = pickId(kindPool, liveArr);
|
|
443
|
+
var id = pick.id;
|
|
212
444
|
if (!id) break;
|
|
213
445
|
|
|
214
|
-
|
|
215
|
-
|
|
446
|
+
var wrap = null;
|
|
447
|
+
|
|
448
|
+
if (pick.recycled && pick.recycled.wrap) {
|
|
449
|
+
// Recycle: only destroy if Ezoic has actually defined this placeholder before.
|
|
450
|
+
if (sessionDefinedIds.has(id)) destroyPlaceholderIds([id]);
|
|
451
|
+
|
|
452
|
+
// Remove old wrapper.
|
|
453
|
+
var oldWrap = pick.recycled.wrap;
|
|
454
|
+
try { if (oldWrap && oldWrap.__ezoicFillObs) oldWrap.__ezoicFillObs.disconnect(); } catch (e) {}
|
|
455
|
+
try { if (oldWrap) oldWrap.remove(); } catch (e) {}
|
|
456
|
+
|
|
457
|
+
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
458
|
+
if (!wrap) continue;
|
|
216
459
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
460
|
+
// Give Ezoic a moment after DOM insertion.
|
|
461
|
+
setTimeoutTracked(function () { callShowAdsWhenReady(id); }, 700);
|
|
462
|
+
} else {
|
|
463
|
+
usedSet.add(id);
|
|
464
|
+
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
465
|
+
if (!wrap) continue;
|
|
466
|
+
|
|
467
|
+
// Micro-delay to allow layout/DOM settle.
|
|
468
|
+
setTimeoutTracked(function () { callShowAdsWhenReady(id); }, 10);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
liveArr.push({ id: id, wrap: wrap });
|
|
472
|
+
|
|
473
|
+
// Final safety: if adjacency happened due to DOM shifts, rollback.
|
|
474
|
+
var prev = wrap && wrap.previousElementSibling;
|
|
475
|
+
var next = wrap && wrap.nextElementSibling;
|
|
476
|
+
if (wrap && ((prev && prev.classList && prev.classList.contains(WRAP_CLASS)) || (next && next.classList && next.classList.contains(WRAP_CLASS)))) {
|
|
477
|
+
try { wrap.remove(); } catch (e) {}
|
|
478
|
+
|
|
479
|
+
if (!(pick.recycled && pick.recycled.wrap)) {
|
|
480
|
+
try { kindPool.unshift(id); } catch (e) {}
|
|
481
|
+
usedSet.delete(id);
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
inserted++;
|
|
220
487
|
}
|
|
221
488
|
|
|
222
489
|
return inserted;
|
|
223
490
|
}
|
|
224
491
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
246
|
-
inserted = injectBetween(
|
|
247
|
-
'ezoic-ad-between',
|
|
248
|
-
getTopicItems(),
|
|
249
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
250
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
251
|
-
state.poolTopics,
|
|
252
|
-
state.usedTopics
|
|
253
|
-
);
|
|
254
|
-
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
255
|
-
inserted = injectBetween(
|
|
256
|
-
'ezoic-ad-category',
|
|
257
|
-
getCategoryItems(),
|
|
258
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 6),
|
|
259
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
260
|
-
state.poolCategories,
|
|
261
|
-
state.usedCategories
|
|
262
|
-
);
|
|
492
|
+
function enforceNoAdjacentAds() {
|
|
493
|
+
var ads = Array.from(document.querySelectorAll('.' + WRAP_CLASS));
|
|
494
|
+
for (var i = 0; i < ads.length; i++) {
|
|
495
|
+
var ad = ads[i];
|
|
496
|
+
var prev = ad.previousElementSibling;
|
|
497
|
+
|
|
498
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
|
|
499
|
+
// Remove adjacent wrapper (do not hide).
|
|
500
|
+
try {
|
|
501
|
+
var ph = ad.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
502
|
+
if (ph) {
|
|
503
|
+
var id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
|
|
504
|
+
if (Number.isFinite(id) && id > 0 && sessionDefinedIds.has(id)) {
|
|
505
|
+
destroyPlaceholderIds([id]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
ad.remove();
|
|
509
|
+
} catch (e) {}
|
|
510
|
+
}
|
|
263
511
|
}
|
|
264
512
|
}
|
|
265
513
|
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
state.
|
|
514
|
+
function cleanup() {
|
|
515
|
+
// Stop any insertion during navigation / DOM teardown.
|
|
516
|
+
state.canShowAds = false;
|
|
517
|
+
state.poolWaitAttempts = 0;
|
|
518
|
+
state.awaitItemsAttempts = 0;
|
|
269
519
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
state.scheduled = false;
|
|
273
|
-
const pk = getPageKey();
|
|
274
|
-
if (state.pageKey && pk !== state.pageKey) return;
|
|
275
|
-
runCore().catch(() => {});
|
|
276
|
-
}, 50);
|
|
277
|
-
}
|
|
520
|
+
// Cancel any pending showAds timeouts.
|
|
521
|
+
clearAllTrackedTimeouts();
|
|
278
522
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
523
|
+
// Disconnect global observer to avoid mutations during teardown.
|
|
524
|
+
if (state.obs) {
|
|
525
|
+
try { state.obs.disconnect(); } catch (e) {}
|
|
526
|
+
state.obs = null;
|
|
527
|
+
}
|
|
282
528
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
529
|
+
// Destroy placeholders that were used (only those that were actually defined).
|
|
530
|
+
destroyUsedPlaceholders();
|
|
286
531
|
|
|
532
|
+
// Remove wrappers from DOM (safe because insertion is now blocked).
|
|
533
|
+
try {
|
|
534
|
+
document.querySelectorAll('.' + WRAP_CLASS).forEach(function (el) {
|
|
535
|
+
try { if (el && el.__ezoicFillObs) el.__ezoicFillObs.disconnect(); } catch (e) {}
|
|
536
|
+
try { el.remove(); } catch (e) {}
|
|
537
|
+
});
|
|
538
|
+
} catch (e) {}
|
|
539
|
+
|
|
540
|
+
// Reset runtime caches.
|
|
287
541
|
state.pageKey = getPageKey();
|
|
288
542
|
state.cfg = null;
|
|
289
543
|
state.cfgPromise = null;
|
|
544
|
+
|
|
290
545
|
state.poolTopics = [];
|
|
291
546
|
state.poolPosts = [];
|
|
292
547
|
state.poolCategories = [];
|
|
548
|
+
|
|
293
549
|
state.usedTopics.clear();
|
|
294
550
|
state.usedPosts.clear();
|
|
295
551
|
state.usedCategories.clear();
|
|
296
|
-
state.lastShowById.clear();
|
|
297
|
-
sessionDefinedIds.clear();
|
|
298
552
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
553
|
+
state.liveTopics = [];
|
|
554
|
+
state.livePosts = [];
|
|
555
|
+
state.liveCategories = [];
|
|
556
|
+
|
|
557
|
+
state.lastShowById.clear();
|
|
558
|
+
state.pendingById.clear();
|
|
559
|
+
insertingIds.clear();
|
|
303
560
|
|
|
304
561
|
state.scheduled = false;
|
|
305
|
-
|
|
306
|
-
|
|
562
|
+
if (state.timer) {
|
|
563
|
+
try { clearTimeout(state.timer); } catch (e) {}
|
|
564
|
+
state.timer = null;
|
|
565
|
+
}
|
|
307
566
|
}
|
|
308
567
|
|
|
309
568
|
function ensureObserver() {
|
|
310
569
|
if (state.obs) return;
|
|
311
|
-
state.obs = new MutationObserver(() => scheduleRun());
|
|
312
570
|
try {
|
|
571
|
+
state.obs = new MutationObserver(function () { scheduleRun('mutation'); });
|
|
313
572
|
state.obs.observe(document.body, { childList: true, subtree: true });
|
|
314
573
|
} catch (e) {}
|
|
315
574
|
}
|
|
316
575
|
|
|
576
|
+
function scheduleRun(/* reason */) {
|
|
577
|
+
if (state.scheduled) return;
|
|
578
|
+
state.scheduled = true;
|
|
579
|
+
|
|
580
|
+
if (state.timer) {
|
|
581
|
+
try { clearTimeout(state.timer); } catch (e) {}
|
|
582
|
+
state.timer = null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
state.timer = setTimeoutTracked(function () {
|
|
586
|
+
state.scheduled = false;
|
|
587
|
+
|
|
588
|
+
// If user navigated away, stop.
|
|
589
|
+
var pk = getPageKey();
|
|
590
|
+
if (state.pageKey && pk !== state.pageKey) return;
|
|
591
|
+
|
|
592
|
+
runCore().catch(function () {});
|
|
593
|
+
}, 80);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function waitForItemsThenRun(kind) {
|
|
597
|
+
// If list isn't in DOM yet (ajaxify transition), retry a bit.
|
|
598
|
+
var count = 0;
|
|
599
|
+
if (kind === 'topic') count = getPostContainers().length;
|
|
600
|
+
else if (kind === 'categoryTopics') count = getTopicItems().length;
|
|
601
|
+
else if (kind === 'categories') count = getCategoryItems().length;
|
|
602
|
+
|
|
603
|
+
if (count > 0) return true;
|
|
604
|
+
|
|
605
|
+
if (state.awaitItemsAttempts < 25) {
|
|
606
|
+
state.awaitItemsAttempts++;
|
|
607
|
+
setTimeoutTracked(function () { scheduleRun('await-items'); }, 120);
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
317
612
|
function waitForContentThenRun() {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
613
|
+
// Avoid inserting ads on pages with too little content.
|
|
614
|
+
var MIN_WORDS = 250;
|
|
615
|
+
var attempts = 0;
|
|
616
|
+
var maxAttempts = 20; // 20 × 200ms = 4s
|
|
617
|
+
|
|
618
|
+
(function check() {
|
|
619
|
+
attempts++;
|
|
620
|
+
|
|
621
|
+
var text = '';
|
|
622
|
+
try { text = document.body.innerText || ''; } catch (e) {}
|
|
623
|
+
var wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
624
|
+
|
|
625
|
+
if (wordCount >= MIN_WORDS) {
|
|
626
|
+
scheduleRun('content-ok');
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (attempts >= maxAttempts) {
|
|
631
|
+
scheduleRun('content-timeout');
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
setTimeoutTracked(check, 200);
|
|
636
|
+
})();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function waitForEzoicThenRun() {
|
|
640
|
+
var attempts = 0;
|
|
641
|
+
var maxAttempts = 50; // 50 × 200ms = 10s
|
|
642
|
+
|
|
643
|
+
(function check() {
|
|
644
|
+
attempts++;
|
|
645
|
+
|
|
646
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
647
|
+
scheduleRun('ezoic-ready');
|
|
648
|
+
waitForContentThenRun();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (attempts >= maxAttempts) {
|
|
653
|
+
scheduleRun('ezoic-timeout');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
setTimeoutTracked(check, 200);
|
|
658
|
+
})();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function fetchConfig() {
|
|
662
|
+
if (state.cfg) return Promise.resolve(state.cfg);
|
|
663
|
+
if (state.cfgPromise) return state.cfgPromise;
|
|
664
|
+
|
|
665
|
+
state.cfgPromise = (function () {
|
|
666
|
+
var MAX_TRIES = 3;
|
|
667
|
+
var delay = 800;
|
|
668
|
+
|
|
669
|
+
function attemptFetch(attempt) {
|
|
670
|
+
return fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' })
|
|
671
|
+
.then(function (res) {
|
|
672
|
+
if (!res || !res.ok) throw new Error('bad response');
|
|
673
|
+
return res.json();
|
|
674
|
+
})
|
|
675
|
+
.then(function (json) {
|
|
676
|
+
state.cfg = json;
|
|
677
|
+
return json;
|
|
678
|
+
})
|
|
679
|
+
.catch(function () {
|
|
680
|
+
if (attempt >= MAX_TRIES) return null;
|
|
681
|
+
return new Promise(function (r) { setTimeoutTracked(r, delay); }).then(function () {
|
|
682
|
+
delay *= 2;
|
|
683
|
+
return attemptFetch(attempt + 1);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return attemptFetch(1).finally(function () { state.cfgPromise = null; });
|
|
689
|
+
})();
|
|
690
|
+
|
|
691
|
+
return state.cfgPromise;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function runCore() {
|
|
695
|
+
// Navigation safety: never insert during ajaxify teardown.
|
|
696
|
+
if (!state.canShowAds) return Promise.resolve();
|
|
697
|
+
|
|
698
|
+
patchShowAds();
|
|
699
|
+
|
|
700
|
+
return fetchConfig().then(function (cfg) {
|
|
701
|
+
if (!cfg || cfg.excluded) return;
|
|
702
|
+
|
|
703
|
+
initPools(cfg);
|
|
704
|
+
|
|
705
|
+
var kind = getKind();
|
|
706
|
+
var inserted = 0;
|
|
707
|
+
|
|
708
|
+
if (!waitForItemsThenRun(kind)) return;
|
|
709
|
+
|
|
710
|
+
if (kind === 'topic') {
|
|
711
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
712
|
+
inserted = injectBetween(
|
|
713
|
+
'ezoic-ad-message',
|
|
714
|
+
getPostContainers(),
|
|
715
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
716
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
717
|
+
state.poolPosts,
|
|
718
|
+
state.usedPosts,
|
|
719
|
+
state.livePosts
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
} else if (kind === 'categoryTopics') {
|
|
723
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
724
|
+
inserted = injectBetween(
|
|
725
|
+
'ezoic-ad-between',
|
|
726
|
+
getTopicItems(),
|
|
727
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
728
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
729
|
+
state.poolTopics,
|
|
730
|
+
state.usedTopics,
|
|
731
|
+
state.liveTopics
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
} else if (kind === 'categories') {
|
|
735
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
736
|
+
inserted = injectBetween(
|
|
737
|
+
'ezoic-ad-categories',
|
|
738
|
+
getCategoryItems(),
|
|
739
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
740
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
741
|
+
state.poolCategories,
|
|
742
|
+
state.usedCategories,
|
|
743
|
+
state.liveCategories
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
enforceNoAdjacentAds();
|
|
749
|
+
|
|
750
|
+
// Recycling: if pool is exhausted, retry a few times to allow old wrappers to scroll off-screen.
|
|
751
|
+
if (inserted === 0) {
|
|
752
|
+
if (state.poolWaitAttempts < 8) {
|
|
753
|
+
state.poolWaitAttempts++;
|
|
754
|
+
setTimeoutTracked(function () { scheduleRun('pool-wait'); }, 400);
|
|
755
|
+
}
|
|
326
756
|
} else {
|
|
327
|
-
|
|
757
|
+
// Reset pool wait attempts once we successfully insert something.
|
|
758
|
+
state.poolWaitAttempts = 0;
|
|
328
759
|
}
|
|
329
|
-
|
|
330
|
-
|
|
760
|
+
|
|
761
|
+
// If we hit max inserts, continue quickly.
|
|
762
|
+
if (inserted >= MAX_INSERTS_PER_RUN) {
|
|
763
|
+
setTimeoutTracked(function () { scheduleRun('continue'); }, 140);
|
|
764
|
+
}
|
|
765
|
+
}).catch(function () {});
|
|
331
766
|
}
|
|
332
767
|
|
|
333
768
|
function bind() {
|
|
334
769
|
if (!$) return;
|
|
335
770
|
|
|
336
771
|
$(window).off('.ezoicInfinite');
|
|
337
|
-
|
|
338
|
-
$(window).on('action:ajaxify.
|
|
772
|
+
|
|
773
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', function () {
|
|
774
|
+
cleanup();
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
$(window).on('action:ajaxify.end.ezoicInfinite', function () {
|
|
339
778
|
state.pageKey = getPageKey();
|
|
340
779
|
ensureObserver();
|
|
341
|
-
|
|
342
|
-
|
|
780
|
+
|
|
781
|
+
// Delay gate to avoid racing NodeBB DOM swap vs Ezoic processing.
|
|
782
|
+
setTimeoutTracked(function () {
|
|
783
|
+
state.canShowAds = true;
|
|
784
|
+
waitForEzoicThenRun();
|
|
785
|
+
}, 300);
|
|
343
786
|
});
|
|
344
787
|
|
|
345
|
-
|
|
788
|
+
// Infinite-scroll and "loaded" events.
|
|
789
|
+
$(window).on('action:category.loaded.ezoicInfinite', function () {
|
|
346
790
|
ensureObserver();
|
|
347
791
|
waitForContentThenRun();
|
|
348
792
|
});
|
|
349
793
|
|
|
350
|
-
$(window).on('action:topics.loaded.ezoicInfinite', ()
|
|
794
|
+
$(window).on('action:topics.loaded.ezoicInfinite', function () {
|
|
351
795
|
ensureObserver();
|
|
352
796
|
waitForContentThenRun();
|
|
353
797
|
});
|
|
354
|
-
}
|
|
355
798
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
799
|
+
$(window).on('action:topic.loaded.ezoicInfinite', function () {
|
|
800
|
+
ensureObserver();
|
|
801
|
+
waitForContentThenRun();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
$(window).on('action:posts.loaded.ezoicInfinite', function () {
|
|
805
|
+
ensureObserver();
|
|
806
|
+
waitForContentThenRun();
|
|
807
|
+
});
|
|
362
808
|
}
|
|
363
809
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
810
|
+
function bindScroll() {
|
|
811
|
+
if (state.lastScrollRun > 0) return;
|
|
812
|
+
state.lastScrollRun = Date.now();
|
|
813
|
+
|
|
814
|
+
var ticking = false;
|
|
815
|
+
window.addEventListener('scroll', function () {
|
|
816
|
+
if (ticking) return;
|
|
817
|
+
ticking = true;
|
|
818
|
+
|
|
819
|
+
window.requestAnimationFrame(function () {
|
|
820
|
+
ticking = false;
|
|
821
|
+
|
|
822
|
+
enforceNoAdjacentAds();
|
|
823
|
+
|
|
824
|
+
// Debounce scheduleRun (max once every 2s on scroll).
|
|
825
|
+
var now = Date.now();
|
|
826
|
+
if (!state.lastScrollRun || (now - state.lastScrollRun > 2000)) {
|
|
827
|
+
state.lastScrollRun = now;
|
|
828
|
+
scheduleRun('scroll');
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
}, { passive: true });
|
|
370
832
|
}
|
|
833
|
+
|
|
834
|
+
// Boot.
|
|
835
|
+
cleanup();
|
|
836
|
+
bind();
|
|
837
|
+
bindScroll();
|
|
838
|
+
ensureObserver();
|
|
839
|
+
|
|
840
|
+
state.pageKey = getPageKey();
|
|
841
|
+
|
|
842
|
+
// Direct page load: allow insertion after initial tick (no ajaxify.end).
|
|
843
|
+
setTimeoutTracked(function () {
|
|
844
|
+
state.canShowAds = true;
|
|
845
|
+
waitForEzoicThenRun();
|
|
846
|
+
}, 0);
|
|
371
847
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
/* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
|
|
1
2
|
.ezoic-ad {
|
|
3
|
+
display: block;
|
|
4
|
+
width: 100%;
|
|
5
|
+
margin: 0 !important;
|
|
2
6
|
padding: 0 !important;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
3
11
|
margin: 0 !important;
|
|
4
|
-
|
|
5
|
-
min-
|
|
12
|
+
padding: 0 !important;
|
|
13
|
+
min-height: 1px; /* keeps placeholder measurable for IO */
|
|
6
14
|
}
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
/* Ezoic sometimes wraps in extra spans/divs with margins */
|
|
17
|
+
.ezoic-ad span.ezoic-ad,
|
|
18
|
+
.ezoic-ad .ezoic-ad {
|
|
19
|
+
margin: 0 !important;
|
|
20
|
+
padding: 0 !important;
|
|
11
21
|
}
|