nodebb-plugin-ezoic-infinite 1.6.6 → 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 +140 -200
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1351,45 +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
|
-
//
|
|
1365
|
-
// -
|
|
1366
|
-
// -
|
|
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.
|
|
1367
1371
|
|
|
1368
|
-
var
|
|
1369
|
-
var
|
|
1370
|
-
var WRAP_SELECTOR = BETWEEN_SELECTOR + ', ' + MESSAGE_SELECTOR;
|
|
1372
|
+
var WRAP_SELECTOR = '.nodebb-ezoic-wrap';
|
|
1373
|
+
var PLACEHOLDER_SELECTOR = '[data-ezoic-id]';
|
|
1371
1374
|
|
|
1372
|
-
|
|
1375
|
+
// show tuning
|
|
1376
|
+
var MAX_SHOW_PER_TICK = 3;
|
|
1373
1377
|
|
|
1374
|
-
//
|
|
1375
|
-
var
|
|
1376
|
-
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;
|
|
1377
1384
|
|
|
1378
|
-
//
|
|
1379
|
-
var
|
|
1385
|
+
// state
|
|
1386
|
+
var canonicalById = Object.create(null); // id -> element
|
|
1387
|
+
var activatedById = Object.create(null); // id -> ts
|
|
1380
1388
|
var showQueue = [];
|
|
1381
1389
|
var showTicking = false;
|
|
1382
1390
|
|
|
1383
|
-
function
|
|
1384
|
-
try { return w.getAttribute('data-ezoic-wrapid'); } catch (e) { return null; }
|
|
1385
|
-
}
|
|
1386
|
-
function getIdFromPlaceholder(w) {
|
|
1391
|
+
function getId(w) {
|
|
1387
1392
|
try {
|
|
1388
|
-
var
|
|
1389
|
-
|
|
1390
|
-
|
|
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;
|
|
1391
1399
|
}
|
|
1392
|
-
function getId(w) { return getIdFromWrap(w) || getIdFromPlaceholder(w); }
|
|
1393
1400
|
|
|
1394
1401
|
function isFilled(w) {
|
|
1395
1402
|
try {
|
|
@@ -1428,227 +1435,160 @@ function buildOrdinalMap(items) {
|
|
|
1428
1435
|
});
|
|
1429
1436
|
}
|
|
1430
1437
|
|
|
1431
|
-
|
|
1432
|
-
function ensureAnchor(w) {
|
|
1438
|
+
function createAnchorForDuplicate(w, id) {
|
|
1433
1439
|
try {
|
|
1434
|
-
if (!w || !w.parentNode) return;
|
|
1435
|
-
if (w.getAttribute('data-ezoic-anchored') === '1') return;
|
|
1436
|
-
|
|
1437
|
-
var wrapUid = w.getAttribute('data-ezoic-wrapuid');
|
|
1438
|
-
if (!wrapUid) {
|
|
1439
|
-
wrapUid = String(Date.now()) + '-' + Math.floor(Math.random() * 1e9);
|
|
1440
|
-
w.setAttribute('data-ezoic-wrapuid', wrapUid);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
// If already preceded by matching anchor, mark anchored.
|
|
1444
|
-
var prev = w.previousSibling;
|
|
1445
|
-
if (prev && prev.nodeType === 1 && prev.classList && prev.classList.contains('nodebb-ezoic-anchor')) {
|
|
1446
|
-
if (prev.getAttribute('data-anchor-for') === wrapUid) {
|
|
1447
|
-
w.setAttribute('data-ezoic-anchored', '1');
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// Create anchor node
|
|
1453
1440
|
var a = document.createElement('span');
|
|
1454
|
-
a.className = 'nodebb-ezoic-anchor';
|
|
1455
|
-
a.setAttribute('data-
|
|
1456
|
-
|
|
1457
|
-
|
|
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';
|
|
1458
1450
|
w.parentNode.insertBefore(a, w);
|
|
1459
|
-
w.
|
|
1451
|
+
w.remove();
|
|
1460
1452
|
} catch (e) {}
|
|
1461
1453
|
}
|
|
1462
1454
|
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
if (now - lastValidate < VALIDATE_COOLDOWN_MS) return;
|
|
1466
|
-
lastValidate = now;
|
|
1467
|
-
|
|
1455
|
+
function normalizeDomOnce() {
|
|
1456
|
+
// Build canonical map; convert duplicates to anchors.
|
|
1468
1457
|
try {
|
|
1469
|
-
|
|
1470
|
-
var wraps = document.querySelectorAll('.nodebb-ezoic-wrap');
|
|
1458
|
+
var wraps = document.querySelectorAll(WRAP_SELECTOR);
|
|
1471
1459
|
for (var i = 0; i < wraps.length; i++) {
|
|
1472
1460
|
var w = wraps[i];
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
if (!uid) {
|
|
1476
|
-
// If it's an old wrap with no uid, anchor it once.
|
|
1477
|
-
ensureAnchor(w);
|
|
1478
|
-
continue;
|
|
1479
|
-
}
|
|
1480
|
-
var prev = w.previousSibling;
|
|
1481
|
-
if (!(prev && prev.nodeType === 1 && prev.classList && prev.classList.contains('nodebb-ezoic-anchor') && prev.getAttribute('data-anchor-for') === uid)) {
|
|
1482
|
-
// Misplaced wrap -> remove it (prevents pile-up before first post)
|
|
1483
|
-
w.remove();
|
|
1484
|
-
}
|
|
1485
|
-
} catch (e) {}
|
|
1486
|
-
}
|
|
1461
|
+
var id = getId(w);
|
|
1462
|
+
if (!id) continue;
|
|
1487
1463
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
a.remove();
|
|
1496
|
-
}
|
|
1497
|
-
} catch (e) {}
|
|
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);
|
|
1470
|
+
}
|
|
1498
1471
|
}
|
|
1499
1472
|
} catch (e) {}
|
|
1500
1473
|
}
|
|
1501
1474
|
|
|
1502
|
-
|
|
1503
|
-
function dedupePlaceholders() {
|
|
1504
|
-
var seen = Object.create(null);
|
|
1475
|
+
function getCandidateAnchors(id) {
|
|
1505
1476
|
try {
|
|
1506
|
-
document.querySelectorAll('.nodebb-ezoic-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
if (!wrap) return;
|
|
1512
|
-
|
|
1513
|
-
// ensure anchor for any wrap we touch
|
|
1514
|
-
ensureAnchor(wrap);
|
|
1477
|
+
return document.querySelectorAll('.nodebb-ezoic-slot-anchor[data-slot-for="' + String(id) + '"]');
|
|
1478
|
+
} catch (e) {
|
|
1479
|
+
return [];
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1515
1482
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
});
|
|
1483
|
+
function anchorDistanceToViewport(a, vh) {
|
|
1484
|
+
try {
|
|
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;
|
|
1524
1490
|
} catch (e) {}
|
|
1491
|
+
return 1e12;
|
|
1525
1492
|
}
|
|
1526
1493
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1494
|
+
function moveCanonicalToAnchor(id, anchor) {
|
|
1495
|
+
var now = Date.now();
|
|
1496
|
+
if (now - lastMove < MOVE_COOLDOWN_MS) return;
|
|
1497
|
+
lastMove = now;
|
|
1531
1498
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (!e || !e.target) return;
|
|
1499
|
+
try {
|
|
1500
|
+
var w = canonicalById[id];
|
|
1501
|
+
if (!w || !anchor || !anchor.parentNode) return;
|
|
1536
1502
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1503
|
+
// If already directly after anchor, nothing to do.
|
|
1504
|
+
if (w.parentNode === anchor.parentNode && w.previousSibling === anchor) return;
|
|
1539
1505
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1506
|
+
// Insert canonical wrap right after anchor.
|
|
1507
|
+
if (anchor.nextSibling) anchor.parentNode.insertBefore(w, anchor.nextSibling);
|
|
1508
|
+
else anchor.parentNode.appendChild(w);
|
|
1509
|
+
} catch (e) {}
|
|
1510
|
+
}
|
|
1543
1511
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1512
|
+
function recycleTick() {
|
|
1513
|
+
var now = Date.now();
|
|
1514
|
+
if (now - lastScan < SCAN_COOLDOWN_MS) return;
|
|
1515
|
+
lastScan = now;
|
|
1516
|
+
|
|
1517
|
+
// ensure dom normalized before recycling
|
|
1518
|
+
normalizeDomOnce();
|
|
1519
|
+
|
|
1520
|
+
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1521
|
+
if (!vh) return;
|
|
1522
|
+
|
|
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) {}
|
|
1549
1551
|
}
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
} catch (e) {}
|
|
1553
1555
|
|
|
1554
|
-
|
|
1556
|
+
scheduleShowTick();
|
|
1555
1557
|
}
|
|
1556
1558
|
|
|
1557
|
-
// Observe
|
|
1559
|
+
// Observe new content; normalize duplicates immediately
|
|
1558
1560
|
var moInstalled = false;
|
|
1559
1561
|
function installMO() {
|
|
1560
1562
|
if (moInstalled || typeof MutationObserver === 'undefined') return;
|
|
1561
1563
|
moInstalled = true;
|
|
1562
1564
|
|
|
1563
1565
|
var mo = new MutationObserver(function (muts) {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
try {
|
|
1568
|
-
for (var i = 0; i < muts.length; i++) {
|
|
1569
|
-
var m = muts[i];
|
|
1570
|
-
if (!m.addedNodes) continue;
|
|
1571
|
-
for (var j = 0; j < m.addedNodes.length; j++) {
|
|
1572
|
-
var n = m.addedNodes[j];
|
|
1573
|
-
if (!n || n.nodeType !== 1) continue;
|
|
1574
|
-
|
|
1575
|
-
if (n.matches && n.matches(WRAP_SELECTOR)) {
|
|
1576
|
-
ensureAnchor(n);
|
|
1577
|
-
try { io.observe(n); } catch (e) {}
|
|
1578
|
-
var id = getId(n);
|
|
1579
|
-
if (id && !isFilled(n)) enqueueShow(id);
|
|
1580
|
-
else if (id) activatedById[id] = activatedById[id] || Date.now();
|
|
1581
|
-
} else if (n.querySelectorAll) {
|
|
1582
|
-
var inner = n.querySelectorAll(WRAP_SELECTOR);
|
|
1583
|
-
for (var k = 0; k < inner.length; k++) {
|
|
1584
|
-
ensureAnchor(inner[k]);
|
|
1585
|
-
try { io.observe(inner[k]); } catch (e) {}
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
} catch (e) {}
|
|
1566
|
+
// When new nodes arrive, normalize and run one recycle tick.
|
|
1567
|
+
normalizeDomOnce();
|
|
1568
|
+
recycleTick();
|
|
1591
1569
|
});
|
|
1592
1570
|
|
|
1593
1571
|
try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
1594
1572
|
}
|
|
1595
1573
|
|
|
1596
1574
|
function init() {
|
|
1597
|
-
|
|
1598
|
-
try {
|
|
1599
|
-
document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureAnchor(w); });
|
|
1600
|
-
} catch (e) {}
|
|
1601
|
-
dedupePlaceholders();
|
|
1602
|
-
|
|
1603
|
-
installIO();
|
|
1575
|
+
normalizeDomOnce();
|
|
1604
1576
|
installMO();
|
|
1605
1577
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1609
|
-
var margin = 900;
|
|
1610
|
-
var wraps = document.querySelectorAll(WRAP_SELECTOR);
|
|
1611
|
-
var budget = 12;
|
|
1612
|
-
for (var i = 0; i < wraps.length && budget > 0; i++) {
|
|
1613
|
-
var w = wraps[i];
|
|
1614
|
-
ensureAnchor(w);
|
|
1615
|
-
var r = w.getBoundingClientRect();
|
|
1616
|
-
if (r.bottom >= -margin && r.top <= (vh + margin)) {
|
|
1617
|
-
var id = getId(w);
|
|
1618
|
-
if (id) {
|
|
1619
|
-
if (isFilled(w)) activatedById[id] = activatedById[id] || Date.now();
|
|
1620
|
-
else enqueueShow(id);
|
|
1621
|
-
}
|
|
1622
|
-
budget--;
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
} catch (e) {}
|
|
1626
|
-
|
|
1627
|
-
// validate regularly on scroll/resize (cheap)
|
|
1628
|
-
window.addEventListener('scroll', function () {
|
|
1629
|
-
validateAnchors();
|
|
1630
|
-
scheduleShowTick();
|
|
1631
|
-
}, { passive: true });
|
|
1632
|
-
window.addEventListener('resize', function () {
|
|
1633
|
-
validateAnchors();
|
|
1634
|
-
scheduleShowTick();
|
|
1635
|
-
}, { passive: true });
|
|
1578
|
+
window.addEventListener('scroll', recycleTick, { passive: true });
|
|
1579
|
+
window.addEventListener('resize', recycleTick, { passive: true });
|
|
1636
1580
|
|
|
1637
1581
|
if (window.jQuery) {
|
|
1638
1582
|
window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
|
|
1639
1583
|
setTimeout(function () {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
validateAnchors();
|
|
1643
|
-
scheduleShowTick();
|
|
1584
|
+
normalizeDomOnce();
|
|
1585
|
+
recycleTick();
|
|
1644
1586
|
}, 0);
|
|
1645
1587
|
});
|
|
1646
1588
|
}
|
|
1647
1589
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
setTimeout(function () { validateAnchors(); scheduleShowTick(); }, 0);
|
|
1590
|
+
setInterval(recycleTick, 900);
|
|
1591
|
+
setTimeout(recycleTick, 0);
|
|
1652
1592
|
}
|
|
1653
1593
|
|
|
1654
1594
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|