nodebb-plugin-ezoic-infinite 1.6.7 → 1.6.8
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/package.json +1 -1
- package/public/client.js +134 -233
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1351,43 +1351,52 @@ function buildOrdinalMap(items) {
|
|
|
1351
1351
|
|
|
1352
1352
|
// ===== CLEAN REFRACTOR: visibility manager for Ezoic wraps =====
|
|
1353
1353
|
(function () {
|
|
1354
|
-
// v2.
|
|
1355
|
-
//
|
|
1356
|
-
// -
|
|
1357
|
-
//
|
|
1354
|
+
// v2.6 (slot-recycler):
|
|
1355
|
+
// Observed:
|
|
1356
|
+
// - When scrolling up, ad wraps accumulate before first post.
|
|
1357
|
+
// - Ezoic logs: "Placeholder Id X already defined" + "No valid placeholders for loadMore"
|
|
1358
1358
|
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1359
|
+
// Root cause:
|
|
1360
|
+
// - Infinite scroll / templating introduces MULTIPLE instances of the SAME placeholder id in DOM.
|
|
1361
|
+
// - Ezoic expects each placeholder id to be unique on the page. Duplicates cause warnings and loadMore failure.
|
|
1362
|
+
// - Some scripts then "re-home" the active slot near the top, causing pile-up.
|
|
1363
1363
|
//
|
|
1364
|
-
//
|
|
1364
|
+
// Fix strategy:
|
|
1365
|
+
// - Keep EXACTLY ONE DOM node per placeholder id ("canonical wrap").
|
|
1366
|
+
// - Replace any duplicate wraps with lightweight SLOT ANCHORS left in place.
|
|
1367
|
+
// - As user scrolls, MOVE the canonical wrap to the closest visible anchor for that id.
|
|
1368
|
+
// (Move, not recreate => no re-define; prevents pile-up; ad follows the viewport.)
|
|
1369
|
+
//
|
|
1370
|
+
// We only handle .nodebb-ezoic-wrap.* nodes; we do not change post markup.
|
|
1365
1371
|
|
|
1366
|
-
var
|
|
1367
|
-
var
|
|
1368
|
-
var WRAP_SELECTOR = BETWEEN_SELECTOR + ', ' + MESSAGE_SELECTOR;
|
|
1372
|
+
var WRAP_SELECTOR = '.nodebb-ezoic-wrap';
|
|
1373
|
+
var PLACEHOLDER_SELECTOR = '[data-ezoic-id]';
|
|
1369
1374
|
|
|
1370
|
-
|
|
1375
|
+
// show tuning
|
|
1376
|
+
var MAX_SHOW_PER_TICK = 3;
|
|
1371
1377
|
|
|
1372
|
-
//
|
|
1373
|
-
var
|
|
1374
|
-
var
|
|
1378
|
+
// recycler tuning
|
|
1379
|
+
var ROOT_MARGIN = 1400; // how far from viewport we consider an anchor "active"
|
|
1380
|
+
var MOVE_COOLDOWN_MS = 120; // avoid moving too often
|
|
1381
|
+
var SCAN_COOLDOWN_MS = 150; // throttle scroll scans
|
|
1382
|
+
var lastScan = 0;
|
|
1383
|
+
var lastMove = 0;
|
|
1375
1384
|
|
|
1376
|
-
//
|
|
1377
|
-
var
|
|
1385
|
+
// state
|
|
1386
|
+
var canonicalById = Object.create(null); // id -> element
|
|
1387
|
+
var activatedById = Object.create(null); // id -> ts
|
|
1378
1388
|
var showQueue = [];
|
|
1379
1389
|
var showTicking = false;
|
|
1380
1390
|
|
|
1381
|
-
function
|
|
1382
|
-
try { return w.getAttribute('data-ezoic-wrapid'); } catch (e) { return null; }
|
|
1383
|
-
}
|
|
1384
|
-
function getIdFromPlaceholder(w) {
|
|
1391
|
+
function getId(w) {
|
|
1385
1392
|
try {
|
|
1386
|
-
var
|
|
1387
|
-
|
|
1388
|
-
|
|
1393
|
+
var id = w.getAttribute('data-ezoic-wrapid');
|
|
1394
|
+
if (id) return id;
|
|
1395
|
+
var ph = w.querySelector(PLACEHOLDER_SELECTOR);
|
|
1396
|
+
if (ph) return ph.getAttribute('data-ezoic-id');
|
|
1397
|
+
} catch (e) {}
|
|
1398
|
+
return null;
|
|
1389
1399
|
}
|
|
1390
|
-
function getId(w) { return getIdFromWrap(w) || getIdFromPlaceholder(w); }
|
|
1391
1400
|
|
|
1392
1401
|
function isFilled(w) {
|
|
1393
1402
|
try {
|
|
@@ -1426,268 +1435,160 @@ function buildOrdinalMap(items) {
|
|
|
1426
1435
|
});
|
|
1427
1436
|
}
|
|
1428
1437
|
|
|
1429
|
-
|
|
1430
|
-
function dedupePlaceholders() {
|
|
1431
|
-
var seen = Object.create(null);
|
|
1438
|
+
function createAnchorForDuplicate(w, id) {
|
|
1432
1439
|
try {
|
|
1433
|
-
document.
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
});
|
|
1440
|
+
var a = document.createElement('span');
|
|
1441
|
+
a.className = 'nodebb-ezoic-slot-anchor';
|
|
1442
|
+
a.setAttribute('data-slot-for', String(id));
|
|
1443
|
+
// keep after/index metadata if present
|
|
1444
|
+
var after = w.getAttribute('data-ezoic-after');
|
|
1445
|
+
if (after) a.setAttribute('data-ezoic-after', after);
|
|
1446
|
+
a.style.display = 'block';
|
|
1447
|
+
a.style.width = '100%';
|
|
1448
|
+
// leave minimal height to keep spacing similar without large blank
|
|
1449
|
+
a.style.minHeight = '1px';
|
|
1450
|
+
w.parentNode.insertBefore(a, w);
|
|
1451
|
+
w.remove();
|
|
1446
1452
|
} catch (e) {}
|
|
1447
1453
|
}
|
|
1448
1454
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1455
|
+
function normalizeDomOnce() {
|
|
1456
|
+
// Build canonical map; convert duplicates to anchors.
|
|
1451
1457
|
try {
|
|
1452
|
-
var
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
w
|
|
1456
|
-
|
|
1457
|
-
return uid;
|
|
1458
|
-
} catch (e) { return null; }
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
function findHostForWrap(w) {
|
|
1462
|
-
// Host should be a stable post/topic element, NOT the wrap itself.
|
|
1463
|
-
// Heuristics: previous siblings that look like topic/post items.
|
|
1464
|
-
try {
|
|
1465
|
-
var cur = w.previousElementSibling;
|
|
1466
|
-
// walk backward a bit
|
|
1467
|
-
for (var i = 0; i < 18 && cur; i++) {
|
|
1468
|
-
if (cur.classList && (cur.classList.contains('topic-item') || cur.classList.contains('posts-list-item') || cur.classList.contains('post') || cur.classList.contains('category-item'))) {
|
|
1469
|
-
return cur;
|
|
1470
|
-
}
|
|
1471
|
-
if (cur.getAttribute) {
|
|
1472
|
-
if (cur.getAttribute('data-pid') || cur.getAttribute('data-tid') || cur.getAttribute('data-topic-id') || cur.getAttribute('data-index')) return cur;
|
|
1473
|
-
}
|
|
1474
|
-
cur = cur.previousElementSibling;
|
|
1475
|
-
}
|
|
1458
|
+
var wraps = document.querySelectorAll(WRAP_SELECTOR);
|
|
1459
|
+
for (var i = 0; i < wraps.length; i++) {
|
|
1460
|
+
var w = wraps[i];
|
|
1461
|
+
var id = getId(w);
|
|
1462
|
+
if (!id) continue;
|
|
1476
1463
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
list = list.parentElement;
|
|
1484
|
-
}
|
|
1485
|
-
if (list && list.querySelectorAll) {
|
|
1486
|
-
var items = list.querySelectorAll('.topic-item, .posts-list-item, .post, [data-pid], [data-tid], [data-topic-id]');
|
|
1487
|
-
if (items && items.length >= afterPos) {
|
|
1488
|
-
return items[Math.min(items.length - 1, afterPos - 1)];
|
|
1489
|
-
}
|
|
1464
|
+
if (!canonicalById[id]) {
|
|
1465
|
+
canonicalById[id] = w;
|
|
1466
|
+
// if already filled, mark activated
|
|
1467
|
+
if (isFilled(w)) activatedById[id] = activatedById[id] || Date.now();
|
|
1468
|
+
} else if (canonicalById[id] !== w) {
|
|
1469
|
+
createAnchorForDuplicate(w, id);
|
|
1490
1470
|
}
|
|
1491
1471
|
}
|
|
1492
1472
|
} catch (e) {}
|
|
1493
|
-
return null;
|
|
1494
1473
|
}
|
|
1495
1474
|
|
|
1496
|
-
function
|
|
1475
|
+
function getCandidateAnchors(id) {
|
|
1497
1476
|
try {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
if (markerId) {
|
|
1503
|
-
var existing = document.getElementById(markerId);
|
|
1504
|
-
if (existing) return;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
var host = findHostForWrap(w);
|
|
1508
|
-
if (!host) return;
|
|
1509
|
-
|
|
1510
|
-
markerId = 'nodebb-ezoic-marker-' + uid;
|
|
1511
|
-
// if already exists somewhere else, reuse
|
|
1512
|
-
var m = document.getElementById(markerId);
|
|
1513
|
-
if (!m) {
|
|
1514
|
-
m = document.createElement('span');
|
|
1515
|
-
m.id = markerId;
|
|
1516
|
-
m.className = 'nodebb-ezoic-marker';
|
|
1517
|
-
m.setAttribute('data-marker-for', uid);
|
|
1518
|
-
m.style.display = 'none';
|
|
1519
|
-
host.appendChild(m);
|
|
1520
|
-
} else {
|
|
1521
|
-
// ensure marker is inside host (if moved, re-append)
|
|
1522
|
-
if (m.parentElement !== host) host.appendChild(m);
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
w.setAttribute('data-ezoic-markerid', markerId);
|
|
1526
|
-
} catch (e) {}
|
|
1477
|
+
return document.querySelectorAll('.nodebb-ezoic-slot-anchor[data-slot-for="' + String(id) + '"]');
|
|
1478
|
+
} catch (e) {
|
|
1479
|
+
return [];
|
|
1480
|
+
}
|
|
1527
1481
|
}
|
|
1528
1482
|
|
|
1529
|
-
function
|
|
1483
|
+
function anchorDistanceToViewport(a, vh) {
|
|
1530
1484
|
try {
|
|
1531
|
-
|
|
1532
|
-
if
|
|
1533
|
-
|
|
1485
|
+
var r = a.getBoundingClientRect();
|
|
1486
|
+
// distance 0 if overlaps extended viewport, else min distance
|
|
1487
|
+
if (r.bottom >= -ROOT_MARGIN && r.top <= (vh + ROOT_MARGIN)) return 0;
|
|
1488
|
+
if (r.top > (vh + ROOT_MARGIN)) return r.top - (vh + ROOT_MARGIN);
|
|
1489
|
+
if (r.bottom < -ROOT_MARGIN) return (-ROOT_MARGIN) - r.bottom;
|
|
1534
1490
|
} catch (e) {}
|
|
1491
|
+
return 1e12;
|
|
1535
1492
|
}
|
|
1536
1493
|
|
|
1537
|
-
function
|
|
1494
|
+
function moveCanonicalToAnchor(id, anchor) {
|
|
1538
1495
|
var now = Date.now();
|
|
1539
|
-
if (now -
|
|
1540
|
-
|
|
1496
|
+
if (now - lastMove < MOVE_COOLDOWN_MS) return;
|
|
1497
|
+
lastMove = now;
|
|
1541
1498
|
|
|
1542
1499
|
try {
|
|
1543
|
-
var
|
|
1544
|
-
|
|
1545
|
-
var w = wraps[i];
|
|
1546
|
-
try {
|
|
1547
|
-
ensureMarkerInHost(w);
|
|
1548
|
-
|
|
1549
|
-
var markerId = w.getAttribute('data-ezoic-markerid');
|
|
1550
|
-
if (!markerId) continue;
|
|
1500
|
+
var w = canonicalById[id];
|
|
1501
|
+
if (!w || !anchor || !anchor.parentNode) return;
|
|
1551
1502
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1503
|
+
// If already directly after anchor, nothing to do.
|
|
1504
|
+
if (w.parentNode === anchor.parentNode && w.previousSibling === anchor) return;
|
|
1554
1505
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
if (w.parentElement !== host.parentElement || w.previousElementSibling !== host) {
|
|
1559
|
-
insertAfter(host, w);
|
|
1560
|
-
}
|
|
1561
|
-
} catch (e) {}
|
|
1562
|
-
}
|
|
1506
|
+
// Insert canonical wrap right after anchor.
|
|
1507
|
+
if (anchor.nextSibling) anchor.parentNode.insertBefore(w, anchor.nextSibling);
|
|
1508
|
+
else anchor.parentNode.appendChild(w);
|
|
1563
1509
|
} catch (e) {}
|
|
1564
1510
|
}
|
|
1565
1511
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
io = new IntersectionObserver(function (entries) {
|
|
1572
|
-
try {
|
|
1573
|
-
entries.forEach(function (e) {
|
|
1574
|
-
if (!e || !e.target) return;
|
|
1512
|
+
function recycleTick() {
|
|
1513
|
+
var now = Date.now();
|
|
1514
|
+
if (now - lastScan < SCAN_COOLDOWN_MS) return;
|
|
1515
|
+
lastScan = now;
|
|
1575
1516
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1517
|
+
// ensure dom normalized before recycling
|
|
1518
|
+
normalizeDomOnce();
|
|
1578
1519
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
if (!id) return;
|
|
1520
|
+
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1521
|
+
if (!vh) return;
|
|
1582
1522
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1523
|
+
try {
|
|
1524
|
+
for (var id in canonicalById) {
|
|
1525
|
+
if (!canonicalById[id]) continue;
|
|
1526
|
+
var anchors = getCandidateAnchors(id);
|
|
1527
|
+
if (!anchors || !anchors.length) continue;
|
|
1528
|
+
|
|
1529
|
+
// choose the first anchor that intersects extended viewport, otherwise closest
|
|
1530
|
+
var best = null;
|
|
1531
|
+
var bestDist = 1e12;
|
|
1532
|
+
for (var i = 0; i < anchors.length; i++) {
|
|
1533
|
+
var a = anchors[i];
|
|
1534
|
+
var d = anchorDistanceToViewport(a, vh);
|
|
1535
|
+
if (d === 0) { best = a; bestDist = 0; break; }
|
|
1536
|
+
if (d < bestDist) { bestDist = d; best = a; }
|
|
1537
|
+
}
|
|
1538
|
+
if (best) {
|
|
1539
|
+
moveCanonicalToAnchor(id, best);
|
|
1540
|
+
|
|
1541
|
+
// trigger show when canonical is near viewport and still empty
|
|
1542
|
+
var w = canonicalById[id];
|
|
1543
|
+
if (w) {
|
|
1544
|
+
try {
|
|
1545
|
+
var r = w.getBoundingClientRect();
|
|
1546
|
+
if (r.bottom >= -ROOT_MARGIN && r.top <= (vh + ROOT_MARGIN)) {
|
|
1547
|
+
if (isFilled(w)) activatedById[id] = activatedById[id] || Date.now();
|
|
1548
|
+
else enqueueShow(id);
|
|
1549
|
+
}
|
|
1550
|
+
} catch (e) {}
|
|
1588
1551
|
}
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
} catch (e) {}
|
|
1592
1555
|
|
|
1593
|
-
|
|
1556
|
+
scheduleShowTick();
|
|
1594
1557
|
}
|
|
1595
1558
|
|
|
1596
|
-
// Observe
|
|
1559
|
+
// Observe new content; normalize duplicates immediately
|
|
1597
1560
|
var moInstalled = false;
|
|
1598
1561
|
function installMO() {
|
|
1599
1562
|
if (moInstalled || typeof MutationObserver === 'undefined') return;
|
|
1600
1563
|
moInstalled = true;
|
|
1601
1564
|
|
|
1602
1565
|
var mo = new MutationObserver(function (muts) {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
try {
|
|
1607
|
-
for (var i = 0; i < muts.length; i++) {
|
|
1608
|
-
var m = muts[i];
|
|
1609
|
-
if (!m.addedNodes) continue;
|
|
1610
|
-
for (var j = 0; j < m.addedNodes.length; j++) {
|
|
1611
|
-
var n = m.addedNodes[j];
|
|
1612
|
-
if (!n || n.nodeType !== 1) continue;
|
|
1613
|
-
|
|
1614
|
-
if (n.matches && n.matches(WRAP_SELECTOR)) {
|
|
1615
|
-
ensureMarkerInHost(n);
|
|
1616
|
-
try { io.observe(n); } catch (e) {}
|
|
1617
|
-
var id = getId(n);
|
|
1618
|
-
if (id && !isFilled(n)) enqueueShow(id);
|
|
1619
|
-
else if (id) activatedById[id] = activatedById[id] || Date.now();
|
|
1620
|
-
} else if (n.querySelectorAll) {
|
|
1621
|
-
var inner = n.querySelectorAll(WRAP_SELECTOR);
|
|
1622
|
-
for (var k = 0; k < inner.length; k++) {
|
|
1623
|
-
ensureMarkerInHost(inner[k]);
|
|
1624
|
-
try { io.observe(inner[k]); } catch (e) {}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
} catch (e) {}
|
|
1630
|
-
|
|
1631
|
-
// enforce after mutations (cheap throttle will apply)
|
|
1632
|
-
enforcePositions();
|
|
1566
|
+
// When new nodes arrive, normalize and run one recycle tick.
|
|
1567
|
+
normalizeDomOnce();
|
|
1568
|
+
recycleTick();
|
|
1633
1569
|
});
|
|
1634
1570
|
|
|
1635
1571
|
try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
1636
1572
|
}
|
|
1637
1573
|
|
|
1638
1574
|
function init() {
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
try {
|
|
1642
|
-
document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureMarkerInHost(w); });
|
|
1643
|
-
} catch (e) {}
|
|
1644
|
-
|
|
1645
|
-
installIO();
|
|
1575
|
+
normalizeDomOnce();
|
|
1646
1576
|
installMO();
|
|
1647
1577
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1651
|
-
var margin = 900;
|
|
1652
|
-
var wraps = document.querySelectorAll(WRAP_SELECTOR);
|
|
1653
|
-
var budget = 12;
|
|
1654
|
-
for (var i = 0; i < wraps.length && budget > 0; i++) {
|
|
1655
|
-
var w = wraps[i];
|
|
1656
|
-
ensureMarkerInHost(w);
|
|
1657
|
-
var r = w.getBoundingClientRect();
|
|
1658
|
-
if (r.bottom >= -margin && r.top <= (vh + margin)) {
|
|
1659
|
-
var id = getId(w);
|
|
1660
|
-
if (id) {
|
|
1661
|
-
if (isFilled(w)) activatedById[id] = activatedById[id] || Date.now();
|
|
1662
|
-
else enqueueShow(id);
|
|
1663
|
-
}
|
|
1664
|
-
budget--;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
} catch (e) {}
|
|
1668
|
-
|
|
1669
|
-
window.addEventListener('scroll', function () {
|
|
1670
|
-
enforcePositions();
|
|
1671
|
-
scheduleShowTick();
|
|
1672
|
-
}, { passive: true });
|
|
1673
|
-
window.addEventListener('resize', function () {
|
|
1674
|
-
enforcePositions();
|
|
1675
|
-
scheduleShowTick();
|
|
1676
|
-
}, { passive: true });
|
|
1578
|
+
window.addEventListener('scroll', recycleTick, { passive: true });
|
|
1579
|
+
window.addEventListener('resize', recycleTick, { passive: true });
|
|
1677
1580
|
|
|
1678
1581
|
if (window.jQuery) {
|
|
1679
1582
|
window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
|
|
1680
1583
|
setTimeout(function () {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
enforcePositions();
|
|
1684
|
-
scheduleShowTick();
|
|
1584
|
+
normalizeDomOnce();
|
|
1585
|
+
recycleTick();
|
|
1685
1586
|
}, 0);
|
|
1686
1587
|
});
|
|
1687
1588
|
}
|
|
1688
1589
|
|
|
1689
|
-
setInterval(
|
|
1690
|
-
setTimeout(
|
|
1590
|
+
setInterval(recycleTick, 900);
|
|
1591
|
+
setTimeout(recycleTick, 0);
|
|
1691
1592
|
}
|
|
1692
1593
|
|
|
1693
1594
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|