nodebb-plugin-ezoic-infinite 0.6.1 → 0.6.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +140 -130
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -1,22 +1,35 @@
1
- /* globals ajaxify */
2
1
  'use strict';
3
2
 
3
+ /* globals ajaxify */
4
4
  window.ezoicInfiniteLoaded = true;
5
5
 
6
6
  let cachedConfig;
7
7
  let lastFetch = 0;
8
8
  let debounceTimer;
9
9
 
10
- let lastSignature = null;
11
10
  let inFlight = false;
12
11
  let rerunRequested = false;
13
12
 
14
- async function fetchConfig() {
15
- if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
16
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
17
- cachedConfig = await res.json();
18
- lastFetch = Date.now();
19
- return cachedConfig;
13
+ // Incremental state (prevents ads "jumping to the top")
14
+ let pageKey = null;
15
+ let injectedSlots = new Set(); // slotNumber per page
16
+ let usedIds = new Set(); // ids currently injected per page
17
+
18
+ function resetPageState() {
19
+ injectedSlots = new Set();
20
+ usedIds = new Set();
21
+ }
22
+
23
+ function currentPageKey() {
24
+ // Stable key per ajaxified page
25
+ try {
26
+ if (ajaxify && ajaxify.data) {
27
+ if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
28
+ if (ajaxify.data.cid) return 'category:' + ajaxify.data.cid;
29
+ if (ajaxify.data.template) return 'tpl:' + ajaxify.data.template + ':' + (ajaxify.data.url || window.location.pathname);
30
+ }
31
+ } catch (e) {}
32
+ return window.location.pathname;
20
33
  }
21
34
 
22
35
  function parsePool(raw) {
@@ -28,44 +41,35 @@ function parsePool(raw) {
28
41
  ));
29
42
  }
30
43
 
31
- function guessIsTopicPage() {
32
- // Prefer ajaxify template, fallback to DOM presence
33
- try {
34
- if (ajaxify && ajaxify.data && ajaxify.data.template) {
35
- return ['topic', 'topicEvents'].includes(ajaxify.data.template);
36
- }
37
- } catch (e) {}
38
- return $('[component="post/content"]').length > 0;
44
+ async function fetchConfig() {
45
+ if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
46
+ const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
47
+ cachedConfig = await res.json();
48
+ lastFetch = Date.now();
49
+ return cachedConfig;
39
50
  }
40
51
 
41
- function guessIsCategoryPage() {
42
- try {
43
- if (ajaxify && ajaxify.data && ajaxify.data.template) {
44
- return ajaxify.data.template === 'category';
45
- }
46
- } catch (e) {}
52
+ function isTopicPage() {
53
+ return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
54
+ }
55
+
56
+ function isCategoryTopicListPage() {
47
57
  return $('li[component="category/topic"]').length > 0;
48
58
  }
49
59
 
50
60
  function getTopicPosts() {
51
- if (!guessIsTopicPage()) return $();
52
-
53
- // Harmony standard wrapper
54
61
  const $primary = $('[component="post"][data-pid]');
55
62
  if ($primary.length) return $primary.not('.ezoic-ad-post');
56
63
 
57
- // Fallback: top-level data-pid that contains post content and isn't nested
58
- const $top = $('[data-pid]').filter(function () {
64
+ return $('[data-pid]').filter(function () {
59
65
  const $el = $(this);
60
66
  const hasContent = $el.find('[component="post/content"]').length > 0;
61
67
  const nested = $el.parents('[data-pid]').length > 0;
62
68
  return hasContent && !nested;
63
- });
64
- return $top.not('.ezoic-ad-post');
69
+ }).not('.ezoic-ad-post');
65
70
  }
66
71
 
67
72
  function getCategoryTopicItems() {
68
- if (!guessIsCategoryPage()) return $();
69
73
  return $('li[component="category/topic"]').not('.ezoic-ad-topic');
70
74
  }
71
75
 
@@ -73,114 +77,122 @@ function tagName($el) {
73
77
  return ($el && $el.length ? (($el.prop('tagName') || '').toUpperCase()) : '');
74
78
  }
75
79
 
76
- function removeExistingEzoicNodes() {
77
- $('.ezoic-ad-post').remove();
78
- $('.ezoic-ad-between').remove();
79
- $('.ezoic-ad-topic').remove();
80
- }
81
-
82
- function computeWindowSlots(totalItems, interval, poolSize) {
83
- const slots = Math.floor(totalItems / interval);
84
- if (slots <= 0) return [];
85
- const start = Math.max(1, slots - poolSize + 1);
86
- const out = [];
87
- for (let s = start; s <= slots; s++) out.push(s);
88
- return out;
89
- }
90
-
91
- function makeWrapperLike($target, classes, innerHtml) {
80
+ function makeWrapperLike($target, classes, innerHtml, attrs) {
92
81
  const t = tagName($target);
82
+ const attrStr = attrs ? ' ' + attrs : '';
93
83
  if (t === 'LI') {
94
- return '<li class="' + classes + ' list-unstyled" data-ezoic-ad="1">' + innerHtml + '</li>';
84
+ return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
95
85
  }
96
- return '<div class="' + classes + '" data-ezoic-ad="1">' + innerHtml + '</div>';
86
+ return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
97
87
  }
98
88
 
99
- function insertBetweenGeneric($items, ids, interval, wrapperClass) {
100
- const total = $items.length;
101
- const slotsToRender = computeWindowSlots(total, interval, ids.length);
102
- if (!slotsToRender.length) return [];
103
-
104
- const activeIds = [];
105
- for (let i = 0; i < slotsToRender.length; i++) {
106
- const slotNumber = slotsToRender[i];
107
- const id = ids[i];
108
- const index = slotNumber * interval - 1;
109
- const $target = $items.eq(index);
110
- if (!$target.length) continue;
111
-
112
- const html = makeWrapperLike($target, wrapperClass, '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>');
113
- $target.after(html);
114
- activeIds.push(id);
89
+ function pickNextId(pool) {
90
+ for (const id of pool) {
91
+ if (!usedIds.has(id)) return id;
115
92
  }
116
- return activeIds;
93
+ return null;
117
94
  }
118
95
 
119
- function insertAdMessagesBetweenReplies($posts, ids, interval) {
120
- const total = $posts.length;
121
- const slotsToRender = computeWindowSlots(total, interval, ids.length);
122
- if (!slotsToRender.length) return [];
123
-
124
- const activeIds = [];
125
- for (let i = 0; i < slotsToRender.length; i++) {
126
- const slotNumber = slotsToRender[i];
127
- const id = ids[i];
128
- const index = slotNumber * interval - 1;
129
- const $target = $posts.eq(index);
130
- if (!$target.length) continue;
131
-
132
- const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
133
- const html = makeWrapperLike($target, 'post ezoic-ad-post', inner);
134
- $target.after(html);
135
- activeIds.push(id);
136
- }
137
- return activeIds;
138
- }
139
-
140
- function ezoicRender(ids) {
96
+ function callEzoic(ids) {
141
97
  if (!ids || !ids.length) return;
142
98
 
143
99
  window.ezstandalone = window.ezstandalone || {};
144
100
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
145
101
 
146
- // Ezoic doc: showAds(101,102,103) within cmd.push for dynamic content citeturn0search0turn0search6
147
- window.ezstandalone.cmd.push(function () {
148
- try {
149
- if (typeof window.ezstandalone.refreshAds === 'function') {
150
- window.ezstandalone.refreshAds.apply(window.ezstandalone, ids);
151
- return;
152
- }
153
- } catch (e) {}
154
-
102
+ const run = function () {
155
103
  try {
156
104
  if (typeof window.ezstandalone.showAds === 'function') {
157
105
  window.ezstandalone.showAds.apply(window.ezstandalone, ids);
158
- return;
106
+ return true;
159
107
  }
160
108
  } catch (e) {}
109
+ return false;
110
+ };
161
111
 
162
- // Last resort: call showAds() with no args = "all placeholders" citeturn0search0turn0search6
163
- try {
164
- if (typeof window.ezstandalone.showAds === 'function') {
165
- window.ezstandalone.showAds();
166
- }
167
- } catch (e) {}
168
- });
112
+ window.ezstandalone.cmd.push(function () { run(); });
113
+
114
+ // retry a few times (Ezoic can load late)
115
+ let tries = 0;
116
+ const maxTries = 6;
117
+ const timer = setInterval(function () {
118
+ tries++;
119
+ if (run() || tries >= maxTries) clearInterval(timer);
120
+ }, 800);
169
121
  }
170
122
 
171
- function computeSignature($posts, $topicItems) {
172
- if ($posts.length) {
173
- const lastPid = $posts.last().attr('data-pid') || '';
174
- return 'topic:' + $posts.length + ':' + lastPid;
123
+ function injectBetweenIncremental($items, pool, interval, wrapperClass) {
124
+ const total = $items.length;
125
+ const maxSlot = Math.floor(total / interval);
126
+ if (maxSlot <= 0) return [];
127
+
128
+ const newIds = [];
129
+
130
+ for (let slot = 1; slot <= maxSlot; slot++) {
131
+ if (injectedSlots.has(slot)) continue;
132
+
133
+ const index = slot * interval - 1;
134
+ const $target = $items.eq(index);
135
+ if (!$target.length) continue;
136
+
137
+ const id = pickNextId(pool);
138
+ if (!id) {
139
+ // pool exhausted: stop injecting further to avoid reusing ids and "jumping"
140
+ break;
141
+ }
142
+
143
+ const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
144
+ const html = makeWrapperLike($target, wrapperClass, placeholder, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
145
+
146
+ $target.after(html);
147
+
148
+ injectedSlots.add(slot);
149
+ usedIds.add(id);
150
+ newIds.push(id);
175
151
  }
176
- if ($topicItems.length) {
177
- const lastTid = $topicItems.last().attr('data-tid') || '';
178
- return 'category:' + $topicItems.length + ':' + lastTid;
152
+
153
+ return newIds;
154
+ }
155
+
156
+ function injectMessageIncremental($posts, pool, interval) {
157
+ const total = $posts.length;
158
+ const maxSlot = Math.floor(total / interval);
159
+ if (maxSlot <= 0) return [];
160
+
161
+ const newIds = [];
162
+
163
+ for (let slot = 1; slot <= maxSlot; slot++) {
164
+ if (injectedSlots.has(slot)) continue;
165
+
166
+ const index = slot * interval - 1;
167
+ const $target = $posts.eq(index);
168
+ if (!$target.length) continue;
169
+
170
+ const id = pickNextId(pool);
171
+ if (!id) break;
172
+
173
+ const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
174
+ const html = makeWrapperLike($target, 'post ezoic-ad-post', inner, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
175
+
176
+ $target.after(html);
177
+
178
+ injectedSlots.add(slot);
179
+ usedIds.add(id);
180
+ newIds.push(id);
179
181
  }
180
- return 'none';
182
+
183
+ return newIds;
181
184
  }
182
185
 
183
186
  async function refreshAds() {
187
+ // reset state when navigating (ajaxify)
188
+ const key = currentPageKey();
189
+ if (pageKey !== key) {
190
+ pageKey = key;
191
+ resetPageState();
192
+ // also cleanup any injected wrappers that may have been left by browser bfcache
193
+ $('.ezoic-ad-post, .ezoic-ad-between, .ezoic-ad-topic').remove();
194
+ }
195
+
184
196
  if (inFlight) { rerunRequested = true; return; }
185
197
  inFlight = true;
186
198
 
@@ -194,49 +206,47 @@ async function refreshAds() {
194
206
  const messagePool = parsePool(cfg.messagePlaceholderIds);
195
207
  const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
196
208
 
197
- const $posts = getTopicPosts();
198
- const $topicItems = getCategoryTopicItems();
209
+ const onTopic = isTopicPage();
210
+ const onCategory = !onTopic && isCategoryTopicListPage();
199
211
 
200
- if (!$posts.length && !$topicItems.length) return;
212
+ const $posts = onTopic ? getTopicPosts() : $();
213
+ const $topicItems = onCategory ? getCategoryTopicItems() : $();
201
214
 
202
- const signature = computeSignature($posts, $topicItems);
203
- if (signature === lastSignature) return;
204
- lastSignature = signature;
205
-
206
- removeExistingEzoicNodes();
215
+ if (!$posts.length && !$topicItems.length) return;
207
216
 
208
- const activeIds = [];
217
+ const newIds = [];
209
218
 
210
- // Category pages: BETWEEN only
219
+ // Your rule:
220
+ // - Category topic list: BETWEEN only
221
+ // - Topic page: MESSAGE only
211
222
  if ($topicItems.length) {
212
223
  if (cfg.enableBetweenAds && betweenPool.length) {
213
- activeIds.push(...insertBetweenGeneric($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
224
+ newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
214
225
  }
215
- ezoicRender(activeIds);
226
+ callEzoic(newIds);
216
227
  return;
217
228
  }
218
229
 
219
- // Topic pages: MESSAGE only (as you requested)
220
230
  if ($posts.length) {
221
231
  if (cfg.enableMessageAds && messagePool.length) {
222
- activeIds.push(...insertAdMessagesBetweenReplies($posts, messagePool, messageInterval));
232
+ newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
223
233
  }
224
- ezoicRender(activeIds);
234
+ callEzoic(newIds);
225
235
  }
226
236
  } finally {
227
237
  inFlight = false;
228
238
  if (rerunRequested) {
229
239
  rerunRequested = false;
230
- setTimeout(refreshAds, 80);
240
+ setTimeout(refreshAds, 120);
231
241
  }
232
242
  }
233
243
  }
234
244
 
235
245
  function debounceRefresh() {
236
246
  clearTimeout(debounceTimer);
237
- debounceTimer = setTimeout(refreshAds, 220);
247
+ debounceTimer = setTimeout(refreshAds, 180);
238
248
  }
239
249
 
240
250
  $(document).ready(debounceRefresh);
241
251
  $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
242
- setTimeout(debounceRefresh, 2200);
252
+ setTimeout(debounceRefresh, 1800);