Last active
June 4, 2026 11:35
-
-
Save lwcorp/81bbb6d5ce80b5f403d68fb38eb8d40d to your computer and use it in GitHub Desktop.
Bookmarklet - Enhance various LinkedIn pages: 1) highlight only relevant notifications 2) Fix auto LTR 3) Reveal timestamps 4) Preview posts 5) Filer posts 6) expose job applies/views
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| javascript:function init(){if("linkedin.com"===location.hostname.replace(/^www\./,""))if(location.pathname.startsWith("/notifications/"))loadAll().then(()=>{grabText("section button:last-of-type","section:nth-of-type(2)>div>div>div>div",'a[href*="/update/')});else if(location.pathname.startsWith("/jobs/view/")||location.search.startsWith("?currentJobId="))initLinkedInJobStats();else{if(!(location.pathname.startsWith("/posts/")||location.pathname.startsWith("/feed/update/")||"/feed/"==location.pathname||"/"==location.pathname||location.pathname.endsWith("/recent-activity/all/")))return void alert("Unsupported page");initDirToggle(),revealLinkedInTimestamps(),previewLinkedinPrepare(),filterPostsPre()}else alert("This script only works on LinkedIn")}async function loadAll(){const e=(()=>{let e=0,t=document.documentElement;const n=document.documentElement;if(n.scrollHeight>n.clientHeight){const t=window.getComputedStyle(n).overflowY,o=window.getComputedStyle(document.body).overflowY;"hidden"!==t&&"hidden"!==o&&(e=n.scrollHeight)}const o=document.body.querySelectorAll("*");for(let n=0;n<o.length;n++){const r=o[n];if(r.clientHeight>0&&r.scrollHeight>r.clientHeight){const n=window.getComputedStyle(r);"auto"!==n.overflowY&&"scroll"!==n.overflowY&&"overlay"!==n.overflowY||r.scrollHeight>e&&(e=r.scrollHeight,t=r)}}return t})(),t=e===document.documentElement||e===document.body;let n;const o=Date.now();for(;;){if(Date.now()-o>1e4){alert("Timeout reached, proceeding with available content");break}if(n=e.scrollHeight,t?window.scrollTo(0,n):e.scrollTo(0,n),await new Promise(e=>setTimeout(e,2e3)),n===e.scrollHeight)break}t?window.scrollTo(0,0):e.scrollTo(0,0),await new Promise(e=>setTimeout(e,1))}function grabText(e,t,n){let o=document.getElementById("highlighterParent");if(!o){o=document.createElement("span"),o.id="highlighterParent";const r=document.createElement("style");r.id="highlightSpecialStyles",r.textContent=".highlightSpecial { background-color: Lavender; } @media (prefers-color-scheme: dark) { .highlightSpecial { background-color: DarkSlateGray; } }",o.appendChild(r),highlighterElement=document.createElement("select"),highlighterElement.id="highlighter",highlighterElement.onchange=()=>stylize(highlighterElement,t,n);[{value:"remove",text:"Keep only what%27s relevant"},{value:"highlight",text:"Highlight only what%27s relevant"},{value:"reset",text:"Reset"},{value:"close",text:"Close this box"}].forEach(e=>{const t=document.createElement("option");t.value=e.value,t.textContent=e.text,highlighterElement.appendChild(t)}),o.appendChild(highlighterElement),document.querySelector(e).after(o),highlighterElement.onchange()}}function stylize(e,t,n){const o=["replied","mentioned you in a","commented"];let r=e.value;"close"==r&&(r="reset");document.querySelectorAll(t).forEach(e=>{const t=e.querySelector(n);let i=!1;if(t){const n=t.textContent.toLowerCase();o.forEach(t=>{if(n.includes(t))switch(i=!0,r){case"highlight":case"remove":e.classList.add("highlightSpecial");break;case"reset":e.classList.remove("highlightSpecial"),e.style.display=""}})}if(!i)switch(r){case"highlight":case"reset":e.style.display="";break;case"remove":e.style.display="none"}}),"close"==e.value&&document.getElementById(e.id).remove()}function log(...e){{const t="[JobStatsExt]";console.log(t,...e)}}function getCsrfToken(){const e=document.cookie.match(/JSESSIONID=(?:"([^"]+)"|([^;]+))/),t=e?e[1]||e[2]:"";return log("CSRF token:",t||"(none found)"),t}async function fetchStats(e,t){log("fetchStats called for jobId:",e);try{const n=await fetch(`/voyager/api/jobs/jobPostings/${e}`,{credentials:"include",headers:{accept:"application/json","csrf-token":t,"x-restli-protocol-version":"2.0.0"}});log("Fetch response status:",n.status);const o=await n.json();log("Raw JSON data:",o);const r=o.applies??o.data?.applies,i=o.views??o.data?.views;return log("Parsed applies/views:",r,i),"number"!=typeof r||"number"!=typeof i?(log("Stats are not numbers, aborting"),null):{applies:r,views:i}}catch(e){return log("Error fetching job stats:",e),null}}async function injectStats(){log("injectStats triggered, current URL path:",location.pathname);const e=location.pathname.match(/^\/jobs\/view\/(\d+)/)||location.search.match(/currentJobId=(\d+)/);if(!e)return void log("Not on a job view page, skipping");const t=e[1];log("Detected jobId:",t),await new Promise(e=>setTimeout(e,1));const n=document.querySelector("p > span:last-child");if(!n)return void log("No title element found");if(n.dataset.statsInjected)return void log("Stats already injected, skipping");log("Title element found:",n),location.search.match(/currentJobId=(\d+)/)||(n.dataset.statsInjected="1");const o=getCsrfToken(),r=await fetchStats(t,o);if(!r)return void log("No stats returned, aborting injection");let{applies:i,views:l}=r;log("Injecting badge with applies/views:",i,l);let a=document.getElementById("badge_container");a&&(log("Removing existing badge"),a.remove()),a=document.createElement("span"),a.id="badge_container";const c=document.createElement("span");let s=l>0?Math.round(i/l*100):0;0==i&&0==l&&(i=l=s="N/A"),c.textContent=`${i} applies (${s}%) โข ${l} views`,c.style.marginLeft="0.5em",c.style.fontSize="0.9em",c.style.color="#666",n.insertAdjacentElement("afterend",a),a.appendChild(c);const d=document.createElement("input");d.type="checkbox",d.id="monitor_stats",d.checked=!0,d.onclick=()=>initLinkedInJobStats();const u=document.createElement("label");u.htmlFor="monitor_stats",u.textContent="Monitor stats",u.style.marginLeft="0.5em",u.style.display="inline",c.insertAdjacentElement("afterend",d),d.insertAdjacentElement("afterend",u),log("Badge injected successfully")}function initLinkedInJobStats(){const e=document.getElementById("monitor_stats");if(e&&!e.checked)return void(window.statsObserver&&(window.statsObserver.disconnect(),log("Observer disconnected due to unchecked checkbox")));e||injectStats(),window.statsObserver&&window.statsObserver.disconnect();let t=location.pathname+location.search;(window.statsObserver=new MutationObserver(()=>{const e=location.pathname+location.search;e!==t&&(t=e,log("URL or search changed, re-running injectStats"),injectStats())})).observe(document.body,{childList:!0,subtree:!0})}function initDirToggle(){if(!document.getElementById("hebrew-ltr-styles")){const e=document.createElement("style");e.id="hebrew-ltr-styles",e.textContent="\n :root { color-scheme: light dark; } \n .hebrew-ltr { \n cursor: crosshair !important;\n transition: background-color 0.2s;\n }\n .hebrew-ltr:not(:hover) {\n background-color: light-dark(sandybrown, darkred); \n }\n ",document.head.appendChild(e)}window._dirToggleObserver&&window._dirToggleObserver.disconnect();const e=e=>{e.currentTarget.setAttribute("dir","rtl"),e.currentTarget.firstElementChild.setAttribute("dir","rtl")},t=e=>{e.currentTarget.setAttribute("dir","ltr"),e.currentTarget.firstElementChild.setAttribute("dir","ltr")},n=()=>{const n=/[\u0590-\u05FF]/;let o='p[componentkey*="-commentary-"]';if(document.querySelectorAll(o).length>0)o=document.querySelectorAll(%60${o}:not([dir="auto"]):not(.hebrew-ltr)%60),o.forEach(o=>{o.querySelector("span")&&n.test(o.textContent)&&(o.classList.add("hebrew-ltr"),o.addEventListener("mouseenter",e),o.addEventListener("mouseleave",t))});else{document.querySelectorAll('div[dir="ltr"]:not(.hebrew-ltr)').forEach(o=>{const r=o.querySelectorAll('span[dir="ltr"]');Array.from(r).some(e=>n.test(e.textContent))&&(o.classList.add("hebrew-ltr"),o.addEventListener("mouseenter",e),o.addEventListener("mouseleave",t))})}};let o;n(),window._dirToggleObserver=new MutationObserver(()=>{clearTimeout(o),o=setTimeout(n,500)}),window._dirToggleObserver.observe(document.body,{childList:!0,subtree:!0})}function revealLinkedInTimestamps(e={}){window._linkedinDateObserver&&window._linkedinDateObserver.disconnect();const t=e.dateFormat||"iso",n=!1!==e.showTime,o=!1!==e.use24HourTime,r={postElements:'div[data-id*="urn:li:activity"], div[data-urn*="urn:li:activity"], a[href*="urn:li:activity"], .feed-shared-update-detail-viewer__right-panel',relativeTime:'.update-components-actor__sub-description span[aria-hidden="true"]',comments:"article.comments-comment-entity",commentTime:"time.comments-comment-meta__data",reposts:".update-components-mini-update-v2, .feed-shared-mini-update-v2, .feed-shared-update-v2__update-content-wrapper"},i=Object.entries(r).filter(([e,t])=>!document.querySelector(t));function l(e){if(!e)return null;try{const t=String(e).replace(/[^0-9]/g,"");if(!t)return null;const n=BigInt(t)>>22n;return Number(n.toString())}catch(e){return null}}function a(e,r){if(!e||!r)return;if(e.getAttribute("data-linkedin-date-revealed"))return;let i=e.getAttribute("data-original-text");i||(i=e.textContent.trim(),e.setAttribute("data-original-text",i));const l=%60${function(e){const r=new Date(e),i=navigator.language||"en-US";let l,a;if(l="iso"===t?r.toISOString().slice(0,10):new Intl.DateTimeFormat(i,{year:"2-digit",month:"2-digit",day:"2-digit"}).format(r),!n)return l;const c={hour:"2-digit",minute:"2-digit",hour12:!o};return a="iso"===t?r.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",hour12:!1}):r.toLocaleTimeString(i,c),%60${l}, ${a} ${r.toLocaleTimeString("en-us",{timeZoneName:"short"}).split(" ").pop()}%60}(r)} โข ${i}%60;Array.from(e.childNodes).forEach(t=>{3===t.nodeType&&e.removeChild(t)}),e.insertBefore(document.createTextNode(l),e.firstChild),e.setAttribute("data-linkedin-date-revealed","true")}i.length>0&&(console.group("[LinkedInHighlighter] Selector Check"),i.forEach(([e,t])=>{console.warn(%60Missing selectors for ${e}: "${t}"%60)}),console.groupEnd());const c=()=>{try{document.querySelectorAll(r.postElements).forEach(e=>{let t=null;const n=e.getAttribute("data-id")||e.getAttribute("data-urn");if(n&&(t=n.split(":").pop().replace(/\D/g,"")),!t){const n=e.querySelector('a[href*="urn:li:activity"]');if(n&&n.href){const e=n.href.match(/urn:li:activity:(\d+)/);e&&(t=e[1])}}if(!t&&window.location.href.includes("/feed/update/urn:li:activity:")){const e=window.location.href.match(/urn:li:activity:(\d+)/);e&&(t=e[1])}if(!t)return;a(e.querySelector(r.relativeTime),l(t))}),document.querySelectorAll(r.reposts).forEach(e=>{const t=e.querySelector(r.relativeTime);if(!t)return;let n=null;const o=e.querySelector('a[href*="urn:li:activity"], a[href*="/feed/update/"]');if(o&&o.href){const e=o.href.match(/urn:li:activity:([0-9]+)/);e&&(n=e[1])}if(!n){const t=e.closest("[data-id], [data-urn]");t&&(n=t.getAttribute("data-id")||t.getAttribute("data-urn"))}if(n){a(t,l(n.split(":").pop().replace(/\D/g,"")))}}),document.querySelectorAll(r.comments).forEach(e=>{const t=e.getAttribute("data-id");if(!t)return;const n=t.match(/(\d+)[^\d]*$/);if(n){a(e.querySelector(r.commentTime),l(n[1]))}})}catch(e){}};let s;c(),window._linkedinDateObserver=new MutationObserver(()=>{clearTimeout(s),s=setTimeout(c,500)}),window._linkedinDateObserver.observe(document.body,{childList:!0,subtree:!0})}function previewLinkedinPrepare(){let e=document.getElementById("grab-wrapper");e&&e.remove(),e=document.createElement("div"),e.id="grab-wrapper",e.setAttribute("style","margin-left: 30% !important; position: relative; z-index: 9999; display: flex; align-items: center; gap: 8px;");const t=document.createElement("button");t.textContent="๐ Preview Tool",t.setAttribute("style","padding: 4px 12px; cursor: pointer; border-radius: 16px; border: 1px solid #0a66c2; background: white; color: #0a66c2; font-weight: 600;");const n=document.createElement("button");n.textContent="โ",n.setAttribute("style","background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-weight: bold; color: white; font-size: 14px; display: flex; align-items: center; justify-content: center; text-shadow: 0px 0px 3px black; transition: background 0.2s;"),n.onmouseover=()=>n.style.background="rgba(255, 0, 0, 0.6)",n.onmouseout=()=>n.style.background="rgba(0,0,0,0.3)",n.onclick=()=>e.remove();const o=document.createElement("div");o.id="grab-content",o.style.display="none",o.style.flexDirection="row",o.style.alignItems="center",o.style.gap="8px";const r=document.createElement("textarea");r.placeholder="Paste potential post here...",r.style.cssText="height: 32px; width: 150px; border-radius: 4px; border: 1px solid #ccc; font-size: 12px; padding: 4px; background: white; color: black;";const i=document.createElement("button");i.textContent="Replace all posts/comments",i.style.cssText="background: #0a66c2; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; height: 32px; font-weight: bold;",i.onclick=()=>previewLinkedinLaunch(r),t.onclick=()=>{const e="none"===o.style.display;o.style.display=e?"flex":"none",t.textContent=e?"Collapse":"๐ Preview Tool"},o.append(r,i),e.append(t,o,n);const l=document.querySelector("#global-nav");l?l.appendChild(e):document.body.prepend(e)}function previewLinkedinLaunch(e){let t='p[componentkey*="-commentary-"]',n=!1;document.querySelectorAll(t).length>0&&(n=!0);const o=document.querySelectorAll(n?%60${t}>span%60:"div>*>span[dir]");if(!e||0===o.length)return e||console.warn("Preview Source not found. Make sure the LinkedIn post modal is open."),void(0===o.length&&console.warn("Preview Target not found. Make sure the LinkedIn post modal is open."));const r=e.value.trim(),i=n?r:r.split("\n").join("<span><br></span>");o.forEach(e=>{n&&e.firstChild&&e.firstChild.nodeType===Node.TEXT_NODE?e.firstChild.textContent=i:e.innerHTML=i})}function filterPostsPre(){let e=document.getElementById("filter-wrapper");e&&e.remove(),e=document.createElement("div"),e.id="filter-wrapper",e.setAttribute("style","margin-left: 30% !important; position: relative; z-index: 9999; display: flex; align-items: center; gap: 8px;");const t=document.createElement("button");t.textContent="๐ Filter by comments",t.setAttribute("style","padding: 4px 12px; cursor: pointer; border-radius: 16px; border: 1px solid #0a66c2; background: white; color: #0a66c2; font-weight: 600;");const n=document.createElement("button");n.textContent="โ",n.setAttribute("style","background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-weight: bold; color: white; font-size: 14px; display: flex; align-items: center; justify-content: center; text-shadow: 0px 0px 3px black; transition: background 0.2s;"),n.onmouseover=()=>n.style.background="rgba(255, 0, 0, 0.6)",n.onmouseout=()=>n.style.background="rgba(0,0,0,0.3)",n.onclick=()=>{filterPostsRun("reset"),e.remove()};const o=document.createElement("div");o.id="filter-content",o.style.cssText="display: none; flex-direction: row; align-items: center; gap: 8px;";const r=document.createElement("select");[">",">=","=","<","<="].forEach(e=>{const t=document.createElement("option");t.value=e,t.textContent=e,r.appendChild(t)});const i=document.createElement("input");i.type="number",i.min="1",i.required=!0,i.value="10",i.style.cssText="width: 60px;";const l=document.createElement("input");l.type="button",l.value="Filter",l.style.cssText="background: #0a66c2; color: white; padding: 4px 12px; cursor: pointer; font-weight: bold;",l.onclick=()=>{loadAll().then(()=>{filterPostsRun(r.value,parseInt(i.value,10))})};const a=document.createElement("input");a.type="button",a.value="Unfilter",a.style.cssText="background: #666; color: white; padding: 4px 12px; cursor: pointer; font-weight: bold;",a.onclick=()=>filterPostsRun("reset"),t.onclick=()=>{const e="none"===o.style.display;o.style.display=e?"flex":"none",t.textContent=e?"Collapse filter":"๐ Filter by comments"},o.append(r,i,l,a),e.append(t,o,n);const c=document.querySelector("#global-nav");c?c.appendChild(e):document.body.prepend(e)}function filterPostsRun(e,t){if(document.querySelectorAll('[data-filtered="true"]').forEach(e=>{e.style.display="",e.removeAttribute("data-filtered")}),"reset"===e)return;const n=document.querySelectorAll("/feed/"==location.pathname?"p":"button"),o=/^(\d+) comments/;n.forEach(n=>{const r=n.textContent.trim().match(o);if(r){const o=parseInt(r[1],10),i=n.closest("/feed/"==location.pathname?'[componentkey^="expanded"]':".artdeco-card");if(i){let n=!1;">"===e&&(n=o>t),">="===e&&(n=o>=t),"="===e&&(n=o===t),"<"===e&&(n=o<t),"<="===e&&(n=o<=t),n||(i.style.display="none",i.setAttribute("data-filtered","true"))}}})}void 0===window.dirToggleEnabled&&(window.dirToggleEnabled=!0),init(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function init() { | |
| if (location.hostname.replace(/^www\./, '') !== 'linkedin.com') { | |
| alert('This script only works on LinkedIn'); | |
| return; | |
| } | |
| if (location.pathname.startsWith('/notifications/')) { | |
| //loadAll('section button:last-of-type', 'section:nth-of-type(2)>div>div>div>div', 'a[href*="/update/'); //'section:nth-of-type(2) a[href*="/update/'); | |
| loadAll().then(() => { | |
| grabText('section button:last-of-type', 'section:nth-of-type(2)>div>div>div>div', 'a[href*="/update/'); | |
| }); | |
| } else if (location.pathname.startsWith('/jobs/view/') || location.search.startsWith('?currentJobId=')) { | |
| initLinkedInJobStats(); | |
| } else if (location.pathname.startsWith('/posts/') || location.pathname.startsWith('/feed/update/') || location.pathname == '/feed/' || location.pathname == '/' || location.pathname.endsWith('/recent-activity/all/')) { // post, homepage or feed page | |
| initDirToggle(); | |
| revealLinkedInTimestamps(); | |
| previewLinkedinPrepare(); | |
| filterPostsPre(); | |
| } else { | |
| alert('Unsupported page'); | |
| return; | |
| } | |
| } | |
| async function loadAll() { | |
| const getScrollContainer = () => { | |
| let maxScrollHeight = 0; | |
| let bestContainer = document.documentElement; // Default to the global window | |
| // Check if the global document is naturally scrollable (classic method) | |
| const docEl = document.documentElement; | |
| if (docEl.scrollHeight > docEl.clientHeight) { | |
| const docOverflow = window.getComputedStyle(docEl).overflowY; | |
| const bodyOverflow = window.getComputedStyle(document.body).overflowY; | |
| if (docOverflow !== 'hidden' && bodyOverflow !== 'hidden') { | |
| maxScrollHeight = docEl.scrollHeight; | |
| } | |
| } | |
| // Next, scan all elements on the page to see if an inner container has a larger scroll area (modern method) | |
| const allElements = document.body.querySelectorAll('*'); | |
| for (let i = 0; i < allElements.length; i++) { | |
| const el = allElements[i]; | |
| // Only calculate computed styles if the element is physically overflowing, which prevents freezing | |
| if (el.clientHeight > 0 && el.scrollHeight > el.clientHeight) { | |
| // If it's overflowing, check if CSS actually allows it to scroll | |
| const style = window.getComputedStyle(el); | |
| if (style.overflowY === 'auto' || style.overflowY === 'scroll' || style.overflowY === 'overlay') { | |
| // Get the container with largest amount of scrollable content | |
| if (el.scrollHeight > maxScrollHeight) { | |
| maxScrollHeight = el.scrollHeight; | |
| bestContainer = el; | |
| } | |
| } | |
| } | |
| } | |
| return bestContainer; | |
| }; | |
| const scrollContainer = getScrollContainer(); | |
| // Determine if we need to use the window to scroll, or the specific element | |
| const isGlobalScroll = (scrollContainer === document.documentElement || scrollContainer === document.body); | |
| let currentHeight; | |
| const startTime = Date.now(); | |
| const timeout = 10; | |
| const delay = 2; | |
| do { | |
| if (Date.now() - startTime > timeout * 1000) { | |
| alert('Timeout reached, proceeding with available content'); | |
| break; | |
| } | |
| currentHeight = scrollContainer.scrollHeight; | |
| // Scroll using the correct method | |
| if (isGlobalScroll) { | |
| window.scrollTo(0, currentHeight); | |
| } else { | |
| scrollContainer.scrollTo(0, currentHeight); | |
| } | |
| await new Promise(resolve => setTimeout(resolve, delay * 1000)); | |
| if (currentHeight === scrollContainer.scrollHeight) { | |
| break; // Reached the bottom | |
| } | |
| } while (true); | |
| // Scroll back to top | |
| if (isGlobalScroll) { | |
| window.scrollTo(0, 0); | |
| } else { | |
| scrollContainer.scrollTo(0, 0); | |
| } | |
| await new Promise(resolve => setTimeout(resolve, 1)); | |
| } | |
| function grabText(btn, parent, item) { | |
| let highlighterParent = document.getElementById('highlighterParent'); | |
| if (!highlighterParent) { | |
| // Create highlighterParent span | |
| highlighterParent = document.createElement('span'); | |
| highlighterParent.id = 'highlighterParent'; | |
| // Add CSS styles with ID | |
| const style = document.createElement('style'); | |
| style.id = 'highlightSpecialStyles'; | |
| style.textContent = '.highlightSpecial { background-color: Lavender; } \ | |
| @media (prefers-color-scheme: dark) { .highlightSpecial { background-color: DarkSlateGray; } }'; | |
| highlighterParent.appendChild(style); | |
| highlighterElement = document.createElement('select'); | |
| highlighterElement.id = 'highlighter'; | |
| highlighterElement.onchange = () => stylize(highlighterElement, parent, item); | |
| /* | |
| highlighterElement.style.position = 'fixed'; | |
| highlighterElement.style.top = '10px'; | |
| highlighterElement.style.right = '10px'; | |
| highlighterElement.style.zIndex = '9999'; | |
| highlighterElement.style.padding = '5px'; | |
| highlighterElement.style.backgroundColor = 'white'; | |
| highlighterElement.style.border = '1px solid #ccc'; | |
| highlighterElement.style.borderRadius = '4px'; | |
| */ | |
| const options = [ | |
| { value: 'remove', text: "Keep only what's relevant" }, | |
| { value: 'highlight', text: "Highlight only what's relevant" }, | |
| { value: 'reset', text: 'Reset' }, | |
| { value: 'close', text: 'Close this box' } | |
| ]; | |
| options.forEach(option => { | |
| const optionElement = document.createElement('option'); | |
| optionElement.value = option.value; | |
| optionElement.textContent = option.text; | |
| highlighterElement.appendChild(optionElement); | |
| }); | |
| highlighterParent.appendChild(highlighterElement); | |
| document.querySelector(btn).after(highlighterParent); | |
| highlighterElement.onchange(); | |
| } | |
| } | |
| function stylize(elm, parent, item) { | |
| const triggerWords = ['replied', 'mentioned you in a', 'commented']; | |
| let theAction = elm.value; | |
| if (theAction == 'close') { | |
| theAction = 'reset'; | |
| } | |
| const parentElements = document.querySelectorAll(parent); | |
| parentElements.forEach(parentEl => { | |
| const itemElement = parentEl.querySelector(item); | |
| let found = false; | |
| if (itemElement) { | |
| const text = itemElement.textContent.toLowerCase(); | |
| triggerWords.forEach(word => { | |
| if (text.includes(word)) { | |
| found = true; | |
| switch(theAction) { | |
| case 'highlight': | |
| case 'remove': | |
| parentEl.classList.add('highlightSpecial'); | |
| break; | |
| case 'reset': | |
| parentEl.classList.remove('highlightSpecial'); | |
| parentEl.style.display = ''; | |
| break; | |
| } | |
| } | |
| }); | |
| } | |
| if (!found) { | |
| switch(theAction) { | |
| case 'highlight': | |
| parentEl.style.display = ''; | |
| break; | |
| case 'remove': | |
| parentEl.style.display = 'none'; | |
| break; | |
| case 'reset': | |
| parentEl.style.display = ''; | |
| break; | |
| } | |
| } | |
| }); | |
| if (elm.value == 'close') { | |
| document.getElementById(elm.id).remove(); | |
| } | |
| } | |
| function log(...args) { | |
| const logging = true; | |
| if (logging) { | |
| const PREFIX = '[JobStatsExt]'; | |
| console.log(PREFIX, ...args); | |
| } | |
| } | |
| // 1. Grab CSRF token from the JSESSIONID cookie | |
| function getCsrfToken() { | |
| const match = document.cookie.match(/JSESSIONID=(?:"([^"]+)"|([^;]+))/); | |
| const token = match ? (match[1] || match[2]) : ''; | |
| log('CSRF token:', token || '(none found)'); | |
| return token; | |
| } | |
| // 2. Fetch applies/views for a given jobId | |
| async function fetchStats(jobId, csrfToken) { | |
| log('fetchStats called for jobId:', jobId); | |
| try { | |
| const resp = await fetch(`/voyager/api/jobs/jobPostings/${jobId}`, { | |
| credentials: 'include', | |
| headers: { | |
| 'accept': 'application/json', | |
| 'csrf-token': csrfToken, | |
| 'x-restli-protocol-version': '2.0.0' | |
| } | |
| }); | |
| /* A bunch of console debugging because this is a weekend project. | |
| A better developer than me (well, any actual developer) will probably replace this with something more robust.*/ | |
| log('Fetch response status:', resp.status); | |
| const data = await resp.json(); | |
| log('Raw JSON data:', data); | |
| const applies = data.applies ?? data.data?.applies; | |
| const views = data.views ?? data.data?.views; | |
| log('Parsed applies/views:', applies, views); | |
| if (typeof applies !== 'number' || typeof views !== 'number') { | |
| log('Stats are not numbers, aborting'); | |
| return null; | |
| } | |
| return { applies, views }; | |
| } catch (e) { | |
| log('Error fetching job stats:', e); | |
| return null; | |
| } | |
| } | |
| // 3. Finds the title, fetches stats, and injects a badge | |
| async function injectStats() { | |
| log('injectStats triggered, current URL path:', location.pathname); | |
| const match = location.pathname.match(/^\/jobs\/view\/(\d+)/) || location.search.match(/currentJobId=(\d+)/); | |
| if (!match) { | |
| log('Not on a job view page, skipping'); | |
| return; | |
| } | |
| const jobId = match[1]; | |
| log('Detected jobId:', jobId); | |
| await new Promise(resolve => setTimeout(resolve, 1)); | |
| const titleEl = document.querySelector('p > span:last-child'); | |
| if (!titleEl) { | |
| log('No title element found'); | |
| return; | |
| } | |
| if (titleEl.dataset.statsInjected) { | |
| log('Stats already injected, skipping'); | |
| return; | |
| } | |
| log('Title element found:', titleEl); | |
| if (!location.search.match(/currentJobId=(\d+)/)) { | |
| titleEl.dataset.statsInjected = '1'; // Prevent double-injection | |
| } | |
| const csrfToken = getCsrfToken(); | |
| const stats = await fetchStats(jobId, csrfToken); | |
| if (!stats) { | |
| log('No stats returned, aborting injection'); | |
| return; | |
| } | |
| let { applies, views } = stats; | |
| log('Injecting badge with applies/views:', applies, views); | |
| let badgeContainer = document.getElementById('badge_container'); | |
| if (badgeContainer) { | |
| log('Removing existing badge'); | |
| badgeContainer.remove(); | |
| } | |
| badgeContainer = document.createElement('span'); | |
| badgeContainer.id = 'badge_container'; | |
| const badge = document.createElement('span'); | |
| let percent = views > 0 ? Math.round((applies / views) * 100) : 0; | |
| if (applies == 0 && views == 0) { | |
| applies = views = percent = 'N/A'; | |
| } | |
| badge.textContent = `${applies} applies (${percent}%) โข ${views} views`; | |
| badge.style.marginLeft = '0.5em'; | |
| badge.style.fontSize = '0.9em'; | |
| badge.style.color = '#666'; | |
| titleEl.insertAdjacentElement('afterend', badgeContainer); | |
| badgeContainer.appendChild(badge); | |
| // Inject a form element | |
| const monitorCheckbox = document.createElement('input'); | |
| monitorCheckbox.type = 'checkbox'; | |
| monitorCheckbox.id = 'monitor_stats'; | |
| monitorCheckbox.checked = true; | |
| monitorCheckbox.onclick = () => initLinkedInJobStats(); | |
| const monitorLabel = document.createElement('label'); | |
| monitorLabel.htmlFor = 'monitor_stats'; | |
| monitorLabel.textContent = 'Monitor stats'; | |
| monitorLabel.style.marginLeft = '0.5em'; | |
| monitorLabel.style.display = 'inline'; | |
| badge.insertAdjacentElement('afterend', monitorCheckbox); | |
| monitorCheckbox.insertAdjacentElement('afterend', monitorLabel); | |
| // monitorCheckbox.insertAdjacentText('afterend', ' Monitor stats'); | |
| log('Badge injected successfully'); | |
| } | |
| // Main function to initialize the LinkedIn job stats extension | |
| function initLinkedInJobStats() { | |
| // Check if checkbox exists and is unchecked - if so, disconnect and return | |
| const monitorCheckbox = document.getElementById('monitor_stats'); | |
| if (monitorCheckbox && !monitorCheckbox.checked) { | |
| if (window.statsObserver) { | |
| window.statsObserver.disconnect(); | |
| log('Observer disconnected due to unchecked checkbox'); | |
| } | |
| return; | |
| } else if (!monitorCheckbox) { | |
| // Initial injection on page load | |
| injectStats(); | |
| } | |
| // 4. Watch for clientโside SPA navigations | |
| // Disconnect existing observer if it exists | |
| if (window.statsObserver) { | |
| window.statsObserver.disconnect(); | |
| } | |
| let lastUrl = location.pathname + location.search; | |
| (window.statsObserver = new MutationObserver(() => { | |
| const currentUrl = location.pathname + location.search; | |
| if (currentUrl !== lastUrl) { | |
| lastUrl = currentUrl; | |
| log('URL or search changed, re-running injectStats'); | |
| injectStats(); | |
| } | |
| })).observe(document.body, { childList: true, subtree: true }); | |
| } | |
| // Initialize dirToggleEnabled only if it doesn't exist | |
| if (typeof window.dirToggleEnabled === 'undefined') { | |
| window.dirToggleEnabled = true; | |
| } | |
| function initDirToggle() { | |
| if (!document.getElementById('hebrew-ltr-styles')) { | |
| const style = document.createElement('style'); | |
| style.id = 'hebrew-ltr-styles'; | |
| style.textContent = ` | |
| :root { color-scheme: light dark; } | |
| .hebrew-ltr { | |
| cursor: crosshair !important; | |
| transition: background-color 0.2s; | |
| } | |
| .hebrew-ltr:not(:hover) { | |
| background-color: light-dark(sandybrown, darkred); | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| if (window._dirToggleObserver) { | |
| window._dirToggleObserver.disconnect(); | |
| } | |
| const handleMouseEnter = (e) => { | |
| e.currentTarget.setAttribute('dir', 'rtl'); | |
| e.currentTarget.firstElementChild.setAttribute('dir', 'rtl'); | |
| }; | |
| const handleMouseLeave = (e) => { | |
| e.currentTarget.setAttribute('dir', 'ltr'); | |
| e.currentTarget.firstElementChild.setAttribute('dir', 'ltr'); | |
| }; | |
| const runDirToggle = () => { | |
| const hebrewRegex = /[\u0590-\u05FF]/; | |
| let pCandidates = 'p[componentkey*="-commentary-"]'; | |
| if (document.querySelectorAll(pCandidates).length > 0) { | |
| pCandidates = document.querySelectorAll( | |
| `${pCandidates}:not([dir="auto"]):not(.hebrew-ltr)` | |
| ); | |
| pCandidates.forEach(p => { | |
| const hasSpan = p.querySelector('span'); | |
| if (hasSpan && hebrewRegex.test(p.textContent)) { | |
| p.classList.add('hebrew-ltr'); | |
| p.addEventListener('mouseenter', handleMouseEnter); | |
| p.addEventListener('mouseleave', handleMouseLeave); | |
| } | |
| }); | |
| } else { | |
| const divCandidates = document.querySelectorAll('div[dir="ltr"]:not(.hebrew-ltr)'); | |
| divCandidates.forEach(el => { | |
| const spans = el.querySelectorAll('span[dir="ltr"]'); | |
| if (Array.from(spans).some(span => hebrewRegex.test(span.textContent))) { | |
| el.classList.add('hebrew-ltr'); | |
| el.addEventListener('mouseenter', handleMouseEnter); | |
| el.addEventListener('mouseleave', handleMouseLeave); | |
| } | |
| }); | |
| } | |
| }; | |
| runDirToggle(); | |
| let timeout; | |
| window._dirToggleObserver = new MutationObserver(() => { | |
| clearTimeout(timeout); | |
| timeout = setTimeout(runDirToggle, 500); | |
| }); | |
| window._dirToggleObserver.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| function revealLinkedInTimestamps(options = {}) { | |
| // 1. Clean up any existing observer from a previous run | |
| if (window._linkedinDateObserver) { | |
| window._linkedinDateObserver.disconnect(); | |
| } | |
| // 2. Configuration | |
| const config = { | |
| dateFormat: options.dateFormat || "iso", | |
| showTime: options.showTime !== false, | |
| use24HourTime: options.use24HourTime !== false, | |
| }; | |
| const SELECTORS = { | |
| // Added the overlay panel class to postElements | |
| postElements: 'div[data-id*="urn:li:activity"], div[data-urn*="urn:li:activity"], a[href*="urn:li:activity"], .feed-shared-update-detail-viewer__right-panel', | |
| relativeTime: '.update-components-actor__sub-description span[aria-hidden="true"]', | |
| comments: "article.comments-comment-entity", | |
| commentTime: "time.comments-comment-meta__data", | |
| reposts: ".update-components-mini-update-v2, .feed-shared-mini-update-v2, .feed-shared-update-v2__update-content-wrapper" | |
| }; | |
| // Check if any selector matches are missing in the document and warn | |
| const missingSelectors = Object.entries(SELECTORS).filter(([key, selector]) => !document.querySelector(selector)); | |
| if (missingSelectors.length > 0) { | |
| console.group('[LinkedInHighlighter] Selector Check'); | |
| missingSelectors.forEach(([key, selector]) => { | |
| console.warn(`Missing selectors for ${key}: "${selector}"`); | |
| }); | |
| console.groupEnd(); | |
| } | |
| // 3. Helpers | |
| function getTimestampFromId(postID) { | |
| if (!postID) return null; | |
| try { | |
| const cleanID = String(postID).replace(/[^0-9]/g, ""); | |
| if (!cleanID) return null; | |
| const bigIntTime = BigInt(cleanID) >> 22n; | |
| return Number(bigIntTime.toString()); | |
| } catch (error) { | |
| return null; | |
| } | |
| } | |
| function getFormattedDate(timestamp) { | |
| const date = new Date(timestamp); | |
| const locale = navigator.language || "en-US"; | |
| let dateStr; | |
| if (config.dateFormat === "iso") { | |
| dateStr = date.toISOString().slice(0, 10); | |
| } else { | |
| dateStr = new Intl.DateTimeFormat(locale, { year: "2-digit", month: "2-digit", day: "2-digit" }).format(date); | |
| } | |
| if (!config.showTime) return dateStr; | |
| let timeStr; | |
| const timeOptions = { hour: "2-digit", minute: "2-digit", hour12: !config.use24HourTime }; | |
| if (config.dateFormat === "iso") { | |
| timeStr = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false }); | |
| } else { | |
| timeStr = date.toLocaleTimeString(locale, timeOptions); | |
| } | |
| const tz = date.toLocaleTimeString("en-us", { timeZoneName: "short" }).split(" ").pop(); | |
| return `${dateStr}, ${timeStr} ${tz}`; | |
| } | |
| function updateNode(element, timestamp) { | |
| if (!element || !timestamp) return; | |
| if (element.getAttribute("data-linkedin-date-revealed")) return; | |
| let original = element.getAttribute("data-original-text"); | |
| if (!original) { | |
| original = element.textContent.trim(); | |
| element.setAttribute("data-original-text", original); | |
| } | |
| const newText = `${getFormattedDate(timestamp)} โข ${original}`; | |
| Array.from(element.childNodes).forEach(node => { | |
| if (node.nodeType === 3) element.removeChild(node); | |
| }); | |
| element.insertBefore(document.createTextNode(newText), element.firstChild); | |
| element.setAttribute("data-linkedin-date-revealed", "true"); | |
| } | |
| // 4. Main Processing Logic | |
| const runUpdate = () => { | |
| try { | |
| // Process Posts (Feed + Overlays) | |
| document.querySelectorAll(SELECTORS.postElements).forEach(post => { | |
| let id = null; | |
| // Strategy 1: Check attributes (Standard Feed) | |
| const dataId = post.getAttribute("data-id") || post.getAttribute("data-urn"); | |
| if (dataId) { | |
| id = dataId.split(":").pop().replace(/\D/g, ""); | |
| } | |
| // Strategy 2: Check internal links (Overlay / Modal) | |
| // Overlays often don't have the ID on the wrapper, but have an "Analytics" or "Share" link inside. | |
| if (!id) { | |
| // Look for any link containing urn:li:activity inside this container | |
| const internalLink = post.querySelector('a[href*="urn:li:activity"]'); | |
| if (internalLink && internalLink.href) { | |
| const match = internalLink.href.match(/urn:li:activity:(\d+)/); | |
| if (match) id = match[1]; | |
| } | |
| } | |
| // Strategy 3: Check URL (Fallback for Overlays) | |
| if (!id && window.location.href.includes("/feed/update/urn:li:activity:")) { | |
| const match = window.location.href.match(/urn:li:activity:(\d+)/); | |
| if (match) id = match[1]; | |
| } | |
| if (!id) return; | |
| const el = post.querySelector(SELECTORS.relativeTime); | |
| updateNode(el, getTimestampFromId(id)); | |
| }); | |
| // Process Reposts | |
| document.querySelectorAll(SELECTORS.reposts).forEach(repost => { | |
| const el = repost.querySelector(SELECTORS.relativeTime); | |
| if (!el) return; | |
| let actId = null; | |
| const link = repost.querySelector('a[href*="urn:li:activity"], a[href*="/feed/update/"]'); | |
| if (link && link.href) { | |
| const match = link.href.match(/urn:li:activity:([0-9]+)/); | |
| if (match) actId = match[1]; | |
| } | |
| if (!actId) { | |
| const cont = repost.closest("[data-id], [data-urn]"); | |
| if (cont) actId = cont.getAttribute("data-id") || cont.getAttribute("data-urn"); | |
| } | |
| if (actId) { | |
| const ts = getTimestampFromId(actId.split(":").pop().replace(/\D/g, "")); | |
| updateNode(el, ts); | |
| } | |
| }); | |
| // Process Comments | |
| document.querySelectorAll(SELECTORS.comments).forEach(comment => { | |
| const dataId = comment.getAttribute("data-id"); | |
| if (!dataId) return; | |
| const match = dataId.match(/(\d+)[^\d]*$/); | |
| if (match) { | |
| const el = comment.querySelector(SELECTORS.commentTime); | |
| updateNode(el, getTimestampFromId(match[1])); | |
| } | |
| }); | |
| } catch (e) { | |
| // Silent fail | |
| } | |
| }; | |
| // 5. Execute immediately | |
| runUpdate(); | |
| // 6. Set up Observer for Infinite Scroll & Modals | |
| let timeout; | |
| window._linkedinDateObserver = new MutationObserver(() => { | |
| clearTimeout(timeout); | |
| timeout = setTimeout(runUpdate, 500); | |
| }); | |
| // Observes body to catch Modals being attached to the DOM | |
| window._linkedinDateObserver.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| function previewLinkedinPrepare() { | |
| const whereSelector = '#global-nav'; | |
| let wrapper = document.getElementById('grab-wrapper'); | |
| if (wrapper) wrapper.remove(); | |
| wrapper = document.createElement('div'); | |
| wrapper.id = 'grab-wrapper'; | |
| wrapper.setAttribute('style', 'margin-left: 30% !important; position: relative; z-index: 9999; display: flex; align-items: center; gap: 8px;'); | |
| const toggleBtn = document.createElement('button'); | |
| toggleBtn.textContent = '\u{1F680} Preview Tool'; | |
| toggleBtn.setAttribute('style', 'padding: 4px 12px; cursor: pointer; border-radius: 16px; border: 1px solid #0a66c2; background: white; color: #0a66c2; font-weight: 600;'); | |
| const closeButton = document.createElement('button'); | |
| closeButton.textContent = '\u2715'; | |
| closeButton.setAttribute('style', 'background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-weight: bold; color: white; font-size: 14px; display: flex; align-items: center; justify-content: center; text-shadow: 0px 0px 3px black; transition: background 0.2s;'); | |
| closeButton.onmouseover = () => closeButton.style.background = 'rgba(255, 0, 0, 0.6)'; | |
| closeButton.onmouseout = () => closeButton.style.background = 'rgba(0,0,0,0.3)'; | |
| closeButton.onclick = () => wrapper.remove(); | |
| const content = document.createElement('div'); | |
| content.id = 'grab-content'; | |
| content.style.display = 'none'; | |
| content.style.flexDirection = 'row'; | |
| content.style.alignItems = 'center'; | |
| content.style.gap = '8px'; | |
| const copyTextArea = document.createElement('textarea'); | |
| copyTextArea.placeholder = "Paste potential post here..."; | |
| copyTextArea.style.cssText = "height: 32px; width: 150px; border-radius: 4px; border: 1px solid #ccc; font-size: 12px; padding: 4px; background: white; color: black;"; | |
| const grabButton = document.createElement('button'); | |
| grabButton.textContent = 'Replace all posts/comments'; | |
| grabButton.style.cssText = "background: #0a66c2; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; height: 32px; font-weight: bold;"; | |
| grabButton.onclick = () => previewLinkedinLaunch(copyTextArea); | |
| toggleBtn.onclick = () => { | |
| const isHidden = content.style.display === 'none'; | |
| content.style.display = isHidden ? 'flex' : 'none'; | |
| toggleBtn.textContent = isHidden ? 'Collapse' : '\u{1F680} Preview Tool'; | |
| }; | |
| content.append(copyTextArea, grabButton); | |
| wrapper.append(toggleBtn, content, closeButton); | |
| const header = document.querySelector(whereSelector); | |
| header ? header.appendChild(wrapper) : document.body.prepend(wrapper); | |
| } | |
| function previewLinkedinLaunch(sourceTextArea) { | |
| let pCandidates = 'p[componentkey*="-commentary-"]', pCandidatesExist = false; | |
| if (document.querySelectorAll(pCandidates).length > 0) { | |
| pCandidatesExist = true; | |
| } | |
| const targets = document.querySelectorAll(pCandidatesExist ? `${pCandidates}>span` : 'div>*>span[dir]'); | |
| if (!sourceTextArea || targets.length === 0) { | |
| if (!sourceTextArea) { | |
| console.warn('Preview Source not found. Make sure the LinkedIn post modal is open.'); | |
| } | |
| if (targets.length === 0) { | |
| console.warn('Preview Target not found. Make sure the LinkedIn post modal is open.'); | |
| } | |
| return; | |
| } | |
| const rawText = sourceTextArea.value.trim(); | |
| const formattedText = pCandidatesExist ? rawText : rawText.split('\n').join('<span><br></span>'); | |
| targets.forEach(target => { | |
| if (pCandidatesExist && target.firstChild && target.firstChild.nodeType === Node.TEXT_NODE) { | |
| target.firstChild.textContent = formattedText; | |
| } else { | |
| target.innerHTML = formattedText; | |
| } | |
| }); | |
| } | |
| function filterPostsPre() { | |
| const whereSelector = '#global-nav'; | |
| let wrapper = document.getElementById('filter-wrapper'); | |
| if (wrapper) wrapper.remove(); | |
| wrapper = document.createElement('div'); | |
| wrapper.id = 'filter-wrapper'; | |
| wrapper.setAttribute('style', 'margin-left: 30% !important; position: relative; z-index: 9999; display: flex; align-items: center; gap: 8px;'); | |
| const toggleBtn = document.createElement('button'); | |
| toggleBtn.textContent = '๐ Filter by comments'; | |
| toggleBtn.setAttribute('style', 'padding: 4px 12px; cursor: pointer; border-radius: 16px; border: 1px solid #0a66c2; background: white; color: #0a66c2; font-weight: 600;'); | |
| const closeButton = document.createElement('button'); | |
| closeButton.textContent = '\u2715'; | |
| closeButton.setAttribute('style', 'background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-weight: bold; color: white; font-size: 14px; display: flex; align-items: center; justify-content: center; text-shadow: 0px 0px 3px black; transition: background 0.2s;'); | |
| closeButton.onmouseover = () => closeButton.style.background = 'rgba(255, 0, 0, 0.6)'; | |
| closeButton.onmouseout = () => closeButton.style.background = 'rgba(0,0,0,0.3)'; | |
| closeButton.onclick = () => { | |
| filterPostsRun('reset'); | |
| wrapper.remove(); | |
| } | |
| const content = document.createElement('div'); | |
| content.id = 'filter-content'; | |
| content.style.cssText = 'display: none; flex-direction: row; align-items: center; gap: 8px;'; | |
| const select = document.createElement('select'); | |
| ['>', '>=', '=', '<', '<='].forEach(op => { | |
| const opt = document.createElement('option'); | |
| opt.value = op; | |
| opt.textContent = op; | |
| select.appendChild(opt); | |
| }); | |
| const numInput = document.createElement('input'); | |
| numInput.type = 'number'; | |
| numInput.min = '1'; | |
| numInput.required = true; | |
| numInput.value = '10'; | |
| numInput.style.cssText = "width: 60px;"; | |
| const filterBtn = document.createElement('input'); | |
| filterBtn.type = 'button'; | |
| filterBtn.value = 'Filter'; | |
| filterBtn.style.cssText = "background: #0a66c2; color: white; padding: 4px 12px; cursor: pointer; font-weight: bold;"; | |
| filterBtn.onclick = () => { | |
| loadAll().then(() => { | |
| filterPostsRun(select.value, parseInt(numInput.value, 10)); | |
| }); | |
| }; | |
| const unfilterBtn = document.createElement('input'); | |
| unfilterBtn.type = 'button'; | |
| unfilterBtn.value = 'Unfilter'; | |
| unfilterBtn.style.cssText = "background: #666; color: white; padding: 4px 12px; cursor: pointer; font-weight: bold;"; | |
| unfilterBtn.onclick = () => filterPostsRun('reset'); | |
| toggleBtn.onclick = () => { | |
| const isHidden = content.style.display === 'none'; | |
| content.style.display = isHidden ? 'flex' : 'none'; | |
| toggleBtn.textContent = isHidden ? 'Collapse filter' : '๐ Filter by comments'; | |
| }; | |
| content.append(select, numInput, filterBtn, unfilterBtn); | |
| wrapper.append(toggleBtn, content, closeButton); | |
| const header = document.querySelector(whereSelector); | |
| header ? header.appendChild(wrapper) : document.body.prepend(wrapper); | |
| } | |
| function filterPostsRun(operator, threshold) { | |
| // Reset previous filtering states | |
| document.querySelectorAll('[data-filtered="true"]').forEach(el => { | |
| el.style.display = ""; | |
| el.removeAttribute('data-filtered'); | |
| }); | |
| if (operator === 'reset') return; | |
| const paragraphs = document.querySelectorAll(location.pathname == '/feed/' ? "p" : 'button'); | |
| const commentRegex = /^(\d+) comments/; | |
| paragraphs.forEach(p => { | |
| const match = p.textContent.trim().match(commentRegex); | |
| if (match) { | |
| const count = parseInt(match[1], 10); | |
| const targetParent = p.closest(location.pathname == '/feed/' ? '[componentkey^="expanded"]' : '.artdeco-card'); | |
| if (targetParent) { | |
| let isMatch = false; | |
| if (operator === '>') isMatch = count > threshold; | |
| if (operator === '>=') isMatch = count >= threshold; | |
| if (operator === '=') isMatch = count === threshold; | |
| if (operator === '<') isMatch = count < threshold; | |
| if (operator === '<=') isMatch = count <= threshold; | |
| if (!isMatch) { | |
| targetParent.style.display = "none"; | |
| targetParent.setAttribute('data-filtered', 'true'); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| init(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment