diff --git a/extension/content.js b/extension/content.js index da4f6d2..ab6e3f5 100644 --- a/extension/content.js +++ b/extension/content.js @@ -29,6 +29,7 @@ function isOnSearchResultsPage() { } const trackerElementsByPost = new WeakMap(); +const trackerAnchorsByPost = new WeakMap(); const postAdditionalNotes = new WeakMap(); const REELS_PATH_PREFIX = '/reel/'; @@ -189,6 +190,52 @@ function clearTrackerElementForPost(postElement, trackerElement = null) { trackerElementsByPost.delete(postElement); } +function getTrackerAnchorForPost(postElement) { + if (!postElement) { + return null; + } + + const anchor = trackerAnchorsByPost.get(postElement); + if (anchor && anchor.isConnected) { + return anchor; + } + + if (anchor) { + trackerAnchorsByPost.delete(postElement); + } + + return null; +} + +function setTrackerAnchorForPost(postElement, anchorElement) { + if (!postElement) { + return; + } + + if (anchorElement && anchorElement.isConnected) { + trackerAnchorsByPost.set(postElement, anchorElement); + } else { + trackerAnchorsByPost.delete(postElement); + } +} + +function clearTrackerAnchorForPost(postElement, anchorElement = null) { + if (!postElement) { + return; + } + + if (!trackerAnchorsByPost.has(postElement)) { + return; + } + + const current = trackerAnchorsByPost.get(postElement); + if (anchorElement && current && current !== anchorElement) { + return; + } + + trackerAnchorsByPost.delete(postElement); +} + function getTrackerScopeRoot(element) { return element ? element.closest(DIALOG_ROOT_SELECTOR) : null; } @@ -361,6 +408,91 @@ function findAdjacentCommentSortAnchor(postElement, actionAnchor = null) { return null; } +function createTrackerInsertionMarker() { + const marker = document.createElement('div'); + marker.className = 'fb-tracker-insertion-anchor'; + marker.setAttribute('aria-hidden', 'true'); + marker.style.cssText = ` + display: block; + width: 100%; + height: 0; + margin: 0; + padding: 0; + border: 0; + pointer-events: none; + `; + return marker; +} + +function positionTrackerInsertionMarker(postElement, marker, buttonBar, postNum = '?') { + if (!postElement || !marker) { + return false; + } + + const resolvedButtonBar = resolveActionButtonBar(postElement, buttonBar); + const actionAnchor = getDirectActionAnchorInPost(postElement, resolvedButtonBar); + const sortAnchor = findAdjacentCommentSortAnchor(postElement, actionAnchor); + const preferredAnchor = sortAnchor || actionAnchor; + + if (preferredAnchor && preferredAnchor.parentElement) { + preferredAnchor.parentElement.insertBefore(marker, preferredAnchor.nextSibling); + return true; + } + + if (resolvedButtonBar && resolvedButtonBar.parentElement && postElement.contains(resolvedButtonBar.parentElement)) { + resolvedButtonBar.parentElement.insertBefore(marker, resolvedButtonBar.nextSibling); + return true; + } + + if (buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement && !postElement.contains(buttonBar.parentElement.parentElement)) { + buttonBar.parentElement.parentElement.insertBefore(marker, buttonBar.parentElement.nextSibling); + return true; + } + + postElement.appendChild(marker); + console.log('[FB Tracker] Post #' + postNum + ' - Marker fallback appended to post'); + return true; +} + +function ensureTrackerInsertionMarker(postElement, buttonBar, postNum = '?') { + if (!postElement) { + return null; + } + + let marker = getTrackerAnchorForPost(postElement); + + if (!marker) { + const existingMarkers = Array.from(postElement.querySelectorAll('.fb-tracker-insertion-anchor')); + if (existingMarkers.length > 0) { + marker = existingMarkers[0]; + existingMarkers.slice(1).forEach((duplicate) => duplicate.remove()); + setTrackerAnchorForPost(postElement, marker); + } + } + + if (!marker || !marker.isConnected) { + marker = createTrackerInsertionMarker(); + } + + const positioned = positionTrackerInsertionMarker(postElement, marker, buttonBar, postNum); + if (!positioned || !marker.isConnected) { + clearTrackerAnchorForPost(postElement, marker); + return null; + } + + setTrackerAnchorForPost(postElement, marker); + return marker; +} + +function insertTrackerAtMarker(marker, trackerElement) { + if (!marker || !trackerElement || !marker.parentElement) { + return false; + } + + marker.parentElement.insertBefore(trackerElement, marker.nextSibling); + return true; +} + const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache const aiCredentialCache = { data: null, @@ -3223,6 +3355,10 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = } if (existingUI) { + const marker = ensureTrackerInsertionMarker(postElement, buttonBar, postNum); + if (marker) { + insertTrackerAtMarker(marker, existingUI); + } console.log('[FB Tracker] Post #' + postNum + ' - UI already exists on normalized element'); postElement.setAttribute(PROCESSED_ATTR, '1'); return; @@ -3252,6 +3388,10 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = if (existingScopedTrackers.length > 0) { const scopedTracker = dedupeTrackersForUrlInScope(encodedUrl, scopeRoot, existingScopedTrackers[0]); if (scopedTracker) { + const marker = ensureTrackerInsertionMarker(postElement, buttonBar, postNum); + if (marker) { + insertTrackerAtMarker(marker, scopedTracker); + } console.log('[FB Tracker] Post #' + postNum + ' - Reusing existing tracker UI in same scope'); setTrackerElementForPost(postElement, scopedTracker); postElement.setAttribute(PROCESSED_ATTR, '1'); @@ -3907,35 +4047,19 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = inserted = tryInsertBeforeReelsCommentComposer(); } - const resolvedButtonBar = resolveActionButtonBar(postElement, buttonBar); - const inPostActionAnchor = getDirectActionAnchorInPost(postElement, resolvedButtonBar); - const commentSortAnchor = findAdjacentCommentSortAnchor(postElement, inPostActionAnchor); - const preferredAnchor = commentSortAnchor || inPostActionAnchor; + const insertionMarker = !inserted + ? ensureTrackerInsertionMarker(postElement, buttonBar, postNum) + : null; - // Strategy 1: Insert below actions; if present, place below the sort row - if (!inserted && preferredAnchor && preferredAnchor.parentElement) { - preferredAnchor.parentElement.insertBefore(container, preferredAnchor.nextSibling); - if (commentSortAnchor) { - console.log('[FB Tracker] Post #' + postNum + ' - UI inserted below comment sort row. ID: #' + container.id); - } else { - console.log('[FB Tracker] Post #' + postNum + ' - UI inserted directly below actions. ID: #' + container.id); + // Strategy 1: Insert at stable marker that is locked below the action bar + if (!inserted && insertionMarker) { + inserted = insertTrackerAtMarker(insertionMarker, container); + if (inserted) { + console.log('[FB Tracker] Post #' + postNum + ' - UI inserted at stable marker. ID: #' + container.id); } - inserted = true; } - // Strategy 2: Insert directly after the detected button bar if still inside the post container - if (!inserted && resolvedButtonBar && resolvedButtonBar.parentElement && postElement.contains(resolvedButtonBar.parentElement)) { - resolvedButtonBar.parentElement.insertBefore(container, resolvedButtonBar.nextSibling); - console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after button bar (in-post fallback). ID: #' + container.id); - inserted = true; - } - // Strategy 3: Legacy fallback for old variants where the interaction bar is outside the post container - if (!inserted && buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement && !postElement.contains(buttonBar.parentElement.parentElement)) { - const grandParent = buttonBar.parentElement.parentElement; - grandParent.insertBefore(container, buttonBar.parentElement.nextSibling); - console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after external button bar parent (legacy). ID: #' + container.id); - inserted = true; - } - // Strategy 4: Append to post element + + // Strategy 2: Last fallback append to post element if (!inserted) { postElement.appendChild(container); console.log('[FB Tracker] Post #' + postNum + ' - UI inserted into article (fallback). ID: #' + container.id); @@ -3966,17 +4090,8 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = console.log('[FB Tracker] Post #' + postNum + ' - UI was removed, re-inserting...'); observer.disconnect(); // Try to re-insert - const currentResolvedButtonBar = resolveActionButtonBar(postElement, buttonBar); - const currentInPostActionAnchor = getDirectActionAnchorInPost(postElement, currentResolvedButtonBar); - const currentCommentSortAnchor = findAdjacentCommentSortAnchor(postElement, currentInPostActionAnchor); - const currentPreferredAnchor = currentCommentSortAnchor || currentInPostActionAnchor; - if (currentPreferredAnchor && currentPreferredAnchor.parentElement) { - currentPreferredAnchor.parentElement.insertBefore(container, currentPreferredAnchor.nextSibling); - } else if (currentResolvedButtonBar && currentResolvedButtonBar.parentElement && postElement.contains(currentResolvedButtonBar.parentElement)) { - currentResolvedButtonBar.parentElement.insertBefore(container, currentResolvedButtonBar.nextSibling); - } else if (buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement && !postElement.contains(buttonBar.parentElement.parentElement)) { - buttonBar.parentElement.parentElement.insertBefore(container, buttonBar.parentElement.nextSibling); - } else if (postElement) { + const currentMarker = ensureTrackerInsertionMarker(postElement, buttonBar, postNum); + if (!currentMarker || !insertTrackerAtMarker(currentMarker, container)) { postElement.appendChild(container); } if (container.isConnected) {