nodebb-plugin-ezoic-infinite 0.6.2 → 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 +111 -89
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.6.2",
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
@@ -5,13 +5,33 @@ window.ezoicInfiniteLoaded = true;
5
5
 
6
6
  let cachedConfig;
7
7
  let lastFetch = 0;
8
-
9
8
  let debounceTimer;
10
- let lastSignature = null;
11
9
 
12
10
  let inFlight = false;
13
11
  let rerunRequested = false;
14
12
 
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;
33
+ }
34
+
15
35
  function parsePool(raw) {
16
36
  if (!raw) return [];
17
37
  return Array.from(new Set(
@@ -29,7 +49,6 @@ async function fetchConfig() {
29
49
  return cachedConfig;
30
50
  }
31
51
 
32
- // Robust DOM-based detection (no dependency on ajaxify template timing)
33
52
  function isTopicPage() {
34
53
  return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
35
54
  }
@@ -42,7 +61,6 @@ function getTopicPosts() {
42
61
  const $primary = $('[component="post"][data-pid]');
43
62
  if ($primary.length) return $primary.not('.ezoic-ad-post');
44
63
 
45
- // Fallback: top-level data-pid containing post/content
46
64
  return $('[data-pid]').filter(function () {
47
65
  const $el = $(this);
48
66
  const hasContent = $el.find('[component="post/content"]').length > 0;
@@ -59,83 +77,22 @@ function tagName($el) {
59
77
  return ($el && $el.length ? (($el.prop('tagName') || '').toUpperCase()) : '');
60
78
  }
61
79
 
62
- function makeWrapperLike($target, classes, innerHtml) {
80
+ function makeWrapperLike($target, classes, innerHtml, attrs) {
63
81
  const t = tagName($target);
82
+ const attrStr = attrs ? ' ' + attrs : '';
64
83
  if (t === 'LI') {
65
- return '<li class="' + classes + ' list-unstyled" data-ezoic-ad="1">' + innerHtml + '</li>';
84
+ return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
66
85
  }
67
- return '<div class="' + classes + '" data-ezoic-ad="1">' + innerHtml + '</div>';
86
+ return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
68
87
  }
69
88
 
70
- function computeWindowSlots(totalItems, interval, poolSize) {
71
- const slots = Math.floor(totalItems / interval);
72
- if (slots <= 0) return [];
73
- const start = Math.max(1, slots - poolSize + 1);
74
- const out = [];
75
- for (let s = start; s <= slots; s++) out.push(s);
76
- return out;
77
- }
78
-
79
- function removeOurWrappers() {
80
- $('.ezoic-ad-post').remove();
81
- $('.ezoic-ad-between').remove();
82
- $('.ezoic-ad-topic').remove();
83
- }
84
-
85
- function insertBetween($items, ids, interval, wrapperClass) {
86
- const total = $items.length;
87
- const slotsToRender = computeWindowSlots(total, interval, ids.length);
88
- if (!slotsToRender.length) return [];
89
-
90
- const activeIds = [];
91
- for (let i = 0; i < slotsToRender.length; i++) {
92
- const slotNumber = slotsToRender[i];
93
- const id = ids[i];
94
- const index = slotNumber * interval - 1;
95
- const $target = $items.eq(index);
96
- if (!$target.length) continue;
97
-
98
- const html = makeWrapperLike($target, wrapperClass, '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>');
99
- $target.after(html);
100
- activeIds.push(id);
89
+ function pickNextId(pool) {
90
+ for (const id of pool) {
91
+ if (!usedIds.has(id)) return id;
101
92
  }
102
- return activeIds;
93
+ return null;
103
94
  }
104
95
 
105
- function insertMessageAds($posts, ids, interval) {
106
- const total = $posts.length;
107
- const slotsToRender = computeWindowSlots(total, interval, ids.length);
108
- if (!slotsToRender.length) return [];
109
-
110
- const activeIds = [];
111
- for (let i = 0; i < slotsToRender.length; i++) {
112
- const slotNumber = slotsToRender[i];
113
- const id = ids[i];
114
- const index = slotNumber * interval - 1;
115
- const $target = $posts.eq(index);
116
- if (!$target.length) continue;
117
-
118
- const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
119
- const html = makeWrapperLike($target, 'post ezoic-ad-post', inner);
120
- $target.after(html);
121
- activeIds.push(id);
122
- }
123
- return activeIds;
124
- }
125
-
126
- function signatureFor($posts, $topicItems) {
127
- if ($posts.length) {
128
- const lastPid = $posts.last().attr('data-pid') || '';
129
- return 'topic:' + $posts.length + ':' + lastPid;
130
- }
131
- if ($topicItems.length) {
132
- const lastTid = $topicItems.last().attr('data-tid') || '';
133
- return 'category:' + $topicItems.length + ':' + lastTid;
134
- }
135
- return 'none';
136
- }
137
-
138
- // Call Ezoic when ready (retry a few times, non-destructive)
139
96
  function callEzoic(ids) {
140
97
  if (!ids || !ids.length) return;
141
98
 
@@ -152,10 +109,9 @@ function callEzoic(ids) {
152
109
  return false;
153
110
  };
154
111
 
155
- // Use cmd queue first (best if Ezoic loads later)
156
112
  window.ezstandalone.cmd.push(function () { run(); });
157
113
 
158
- // Also retry a few times in case cmd isn't flushed promptly
114
+ // retry a few times (Ezoic can load late)
159
115
  let tries = 0;
160
116
  const maxTries = 6;
161
117
  const timer = setInterval(function () {
@@ -164,7 +120,79 @@ function callEzoic(ids) {
164
120
  }, 800);
165
121
  }
166
122
 
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);
151
+ }
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);
181
+ }
182
+
183
+ return newIds;
184
+ }
185
+
167
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
+
168
196
  if (inFlight) { rerunRequested = true; return; }
169
197
  inFlight = true;
170
198
 
@@ -186,45 +214,39 @@ async function refreshAds() {
186
214
 
187
215
  if (!$posts.length && !$topicItems.length) return;
188
216
 
189
- const sig = signatureFor($posts, $topicItems);
190
- if (sig === lastSignature) return;
191
- lastSignature = sig;
192
-
193
- removeOurWrappers();
194
-
195
- const activeIds = [];
217
+ const newIds = [];
196
218
 
197
219
  // Your rule:
198
220
  // - Category topic list: BETWEEN only
199
221
  // - Topic page: MESSAGE only
200
222
  if ($topicItems.length) {
201
223
  if (cfg.enableBetweenAds && betweenPool.length) {
202
- activeIds.push(...insertBetween($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
224
+ newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
203
225
  }
204
- callEzoic(activeIds);
226
+ callEzoic(newIds);
205
227
  return;
206
228
  }
207
229
 
208
230
  if ($posts.length) {
209
231
  if (cfg.enableMessageAds && messagePool.length) {
210
- activeIds.push(...insertMessageAds($posts, messagePool, messageInterval));
232
+ newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
211
233
  }
212
- callEzoic(activeIds);
234
+ callEzoic(newIds);
213
235
  }
214
236
  } finally {
215
237
  inFlight = false;
216
238
  if (rerunRequested) {
217
239
  rerunRequested = false;
218
- setTimeout(refreshAds, 80);
240
+ setTimeout(refreshAds, 120);
219
241
  }
220
242
  }
221
243
  }
222
244
 
223
245
  function debounceRefresh() {
224
246
  clearTimeout(debounceTimer);
225
- debounceTimer = setTimeout(refreshAds, 200);
247
+ debounceTimer = setTimeout(refreshAds, 180);
226
248
  }
227
249
 
228
250
  $(document).ready(debounceRefresh);
229
251
  $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
230
- setTimeout(debounceRefresh, 2000);
252
+ setTimeout(debounceRefresh, 1800);