Greasy Fork 还支持 简体中文。

LinkedIn Job Search Usability Improvements

Make it easier to review and manage job search results, with faster keyboard shortcuts, read post tracking, and blacklists for companies and jobs

目前為 2020-01-18 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name LinkedIn Job Search Usability Improvements
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.7
  5. // @description Make it easier to review and manage job search results, with faster keyboard shortcuts, read post tracking, and blacklists for companies and jobs
  6. // @author Bryan Chan
  7. // @match https://www.linkedin.com/jobs/search/*
  8. // @license GNU GPLv3
  9. // @grant GM_addStyle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. console.log("Starting LinkedIn Job Search Usability Improvements");
  18.  
  19. // Setup dictionaries to persist useful information across sessions
  20. class StoredDictionary {
  21. constructor(storageKey) {
  22. this.storageKey = storageKey;
  23. this.data = GM_getValue(storageKey) || {};
  24. console.log("Initial data read from", this.storageKey, this.data);
  25. }
  26.  
  27. get(key) {
  28. return this.data[key];
  29. }
  30.  
  31. set(key, value) {
  32. this.data[key] = value;
  33. GM_setValue(this.storageKey, this.data);
  34. console.log("Updated data", this.storageKey, this.data);
  35. }
  36.  
  37. getDictionary() {
  38. return this.data;
  39. }
  40. }
  41.  
  42. const hiddenCompanies = new StoredDictionary("hidden_companies");
  43. const hiddenPosts = new StoredDictionary("hidden_posts");
  44. const readPosts = new StoredDictionary("read_posts");
  45.  
  46. /** Install key handlers to allow for keyboard interactions */
  47. const KEY_HANDLER = {
  48. "e": handleMarkRead, // mark the active post as read
  49. "j": goToNext, // open the next visible job post
  50. "k": goToPrevious, // open the previous visible job post
  51. "h": toggleHidden, // toggle showing the hidden posts
  52. "n": handleNextPage, // go to the next page
  53. "p": handlePrevPage, // go to the previous page
  54. "x": handleHidePost, // hide post forever
  55. "y": handleHideCompany, // hide company forever
  56. "?": handlePrintDebug, // print debug information to the console
  57. }
  58.  
  59. window.addEventListener("keydown", function(e) {
  60. const handler = KEY_HANDLER[e.key]
  61. if(handler) handler();
  62. });
  63.  
  64. /** Event handler functions */
  65. const FEEDBACK_DELAY = 300;
  66.  
  67. // Toggle whether to hide posts
  68. var showHidden = false;
  69. function toggleHidden() {
  70. showHidden = !showHidden;
  71. queueUpdate();
  72. }
  73.  
  74. // Handle a request to hide a post forever
  75. function handleHidePost() {
  76. const activeJob = getActive();
  77. const data = getCardData(activeJob);
  78.  
  79. // Show feedback
  80. activeJob.style.opacity = 0.6;
  81. const postTitle = getPostNode(activeJob);
  82. postTitle.style.textDecoration = "line-through";
  83.  
  84. const detailPostTitle = document.querySelector(".jobs-details-top-card__job-title");
  85. detailPostTitle.style.textDecoration = "line-through";
  86.  
  87. // Wait a little and then hide post
  88. setTimeout(() => {
  89. goToNext();
  90. detailPostTitle.style.textDecoration = "none";
  91. hiddenPosts.set(data.postUrl, `${data.companyName}: ${data.postTitle}`);
  92. updateDisplay();
  93. }, FEEDBACK_DELAY);
  94. }
  95.  
  96. // Handle request to hide all posts from a company, forever
  97. function handleHideCompany() {
  98. const activeJob = getActive();
  99. const data = getCardData(activeJob);
  100.  
  101. // show feedback
  102. activeJob.style.opacity = 0.6;
  103. const company = getCompanyNode(activeJob);
  104. company.style.textDecoration = "line-through";
  105.  
  106. const detailCompany = document.querySelector(".jobs-details-top-card__company-url");
  107. detailCompany.style.textDecoration = "line-through";
  108.  
  109. // Wait a little and then hide company
  110. setTimeout(() => {
  111. // go to next post and hide the company
  112. goToNext();
  113. detailCompany.style.textDecoration = "none";
  114. hiddenCompanies.set(data.companyUrl, data.companyName);
  115. updateDisplay();
  116. }, FEEDBACK_DELAY);
  117. }
  118.  
  119. const PAGE_DELAY = 300; // delay after loading new page to go to the first element
  120. function handleNextPage() {
  121. const activePage = document.querySelector(".artdeco-pagination__indicator--number.active");
  122. if(!activePage) return;
  123. const nextPage = activePage.nextElementSibling.firstElementChild;
  124. triggerClick(nextPage);
  125. }
  126.  
  127. function handlePrevPage() {
  128. const activePage = document.querySelector(".artdeco-pagination__indicator--number.active");
  129. if(!activePage) return;
  130. const prevPage = activePage.previousElementSibling.firstElementChild;
  131. triggerClick(prevPage);
  132. }
  133.  
  134.  
  135. // Handl request to mark a post as read (
  136. function handleMarkRead() {
  137. // @TODO implement this in a useful way
  138. const activeJob = getActive();
  139. const data = getCardData(activeJob);
  140. goToNext();
  141. readPosts.set(data.postUrl, `${data.companyName}: ${data.postTitle}`);
  142. updateDisplay();
  143. }
  144.  
  145. // Handle requests to print debug information
  146. function handlePrintDebug() {
  147.  
  148. console.log("Hidden companies");
  149. console.log(hiddenCompanies.getDictionary());
  150.  
  151. console.log("Hidden posts");
  152. console.log(hiddenPosts.getDictionary());
  153.  
  154. console.log("Read posts");
  155. console.log(readPosts.getDictionary());
  156. }
  157.  
  158. /** Functions to adjust jobs list display, based on which companies, posts are hidden and which posts are read */
  159. const jobsList = document.querySelector("ul.jobs-search-results__list");
  160. var updateQueued = false;
  161. var updateTimer = null;
  162. function queueUpdate() {
  163. if(updateTimer) {
  164. clearTimeout(updateTimer);
  165. }
  166. updateTimer = setTimeout(function() {
  167. updateTimer = null;
  168. updateDisplay()
  169. }, 30);
  170. }
  171. function updateDisplay() {
  172. const start = +new Date();
  173. for(var job = jobsList.firstElementChild; job.nextSibling; job = job.nextSibling.nextSibling) {
  174. try {
  175. const data = getCardData(job);
  176. const jobDiv = job.firstElementChild;
  177.  
  178. if(showHidden) {
  179. jobDiv.classList.remove("hidden");
  180. continue;
  181. }
  182.  
  183. if(hiddenCompanies.get(data.companyUrl)) {
  184. jobDiv.classList.add("hidden");
  185. } else if(hiddenPosts.get(data.postUrl)) {
  186. jobDiv.classList.add("hidden");
  187. } else if(readPosts.get(data.postUrl)) {
  188. jobDiv.classList.add("read");
  189. }
  190.  
  191. } catch(e) {
  192. }
  193. }
  194. const elapsed = +new Date() - start;
  195. console.log("Updated display on jobs list in", elapsed, "ms");
  196. }
  197.  
  198. function triggerMouseEvent (node, eventType) {
  199. var clickEvent = document.createEvent ('MouseEvents');
  200. clickEvent.initEvent (eventType, true, true);
  201. node.dispatchEvent (clickEvent);
  202. }
  203.  
  204. /** Get active job card */
  205. function getActive() {
  206. const active = document.querySelector(".job-card-search--is-active");
  207. return active ? active.parentNode : undefined;
  208. }
  209.  
  210. /** Select first card in the list */
  211. function goToFirst() {
  212. const firstPost = jobsList.firstElementChild;
  213. const clickableDiv = firstPost.firstElementChild;
  214. triggerClick(clickableDiv);
  215. }
  216.  
  217. function goToNext() {
  218. const active = getActive();
  219. if(active) {
  220. var next = active.nextSibling.nextSibling;
  221. while(next.firstElementChild && isHidden(next.firstElementChild)) {
  222. next = next.nextSibling.nextSibling;
  223. }
  224. if(next.firstElementChild) {
  225. triggerClick(next.firstElementChild);
  226. } else { // no next job, try for the next page
  227. handleNextPage();
  228. }
  229. } else {
  230. goToFirst();
  231. }
  232. }
  233.  
  234. function goToPrevious() {
  235. const active = getActive();
  236. if(active) {
  237. var prev = active.previousSibling.previousSibling;
  238. while(prev.firstElementChild && isHidden(prev.firstElementChild)) {
  239. prev = prev.previousSibling.previousSibling;
  240. }
  241. if(prev.firstElementChild) {
  242. triggerClick(prev.firstElementChild);
  243. } else { // no previous job, try to go to the previous page
  244. handlePrevPage();
  245. }
  246. } else {
  247. goToFirst();
  248. }
  249. }
  250.  
  251. function triggerClick (node) {
  252. triggerMouseEvent (node, "mouseover");
  253. triggerMouseEvent (node, "mousedown");
  254. triggerMouseEvent (node, "mouseup");
  255. triggerMouseEvent (node, "click");
  256. }
  257.  
  258. /** Check if a card is hidden */
  259. function isHidden (node) {
  260. return node.classList.contains("jobs-search-results-feedback") ||
  261. node.classList.contains("hidden");
  262. }
  263.  
  264. /** Extracts card data from a card */
  265. function getCompanyNode (node) {
  266. return node.querySelector("a.job-card-search__company-name-link")
  267. }
  268. function getPostNode (node) {
  269. return node.querySelector(".job-card-search__title a.job-card-search__link-wrapper")
  270. }
  271. function getCardData (node) {
  272. var companyUrl, companyName, postUrl, postTitle;
  273. const company = getCompanyNode(node);
  274. if(company) {
  275. companyUrl = company.getAttribute("href");
  276. companyName = company.text.trim(" ");
  277. }
  278.  
  279. const post = getPostNode(node);
  280. if(post) {
  281. postUrl = post.getAttribute("href").split("/?")[0];
  282. postTitle = post.text.replace("Promoted","").trim(" \n");
  283. }
  284. return {
  285. companyUrl,
  286. companyName,
  287. postUrl,
  288. postTitle
  289. };
  290. }
  291.  
  292. /** Add styles to handle hiding */
  293. GM_addStyle(".jobs-search-results-feedback { display: none }");
  294. GM_addStyle(".hidden { display: none }");
  295. GM_addStyle(".read { opacity: 0.3 }");
  296.  
  297.  
  298. console.log("Adding mutation observer");
  299.  
  300. // Options for the observer (which mutations to observe)
  301. const config = { attributes: true, childList: true, subtree: true };
  302.  
  303. // Callback function to execute when mutations are observed
  304. const callback = function(mutationsList, observer) {
  305. // Use traditional 'for loops' for IE 11
  306. for(let mutation of mutationsList) {
  307. const target = mutation.target;
  308. if (mutation.type === 'childList') {
  309. queueUpdate();
  310. }
  311. else if (mutation.type === 'attributes') {
  312. //console.log('The ' + mutation.attributeName + ' attribute was modified.', target);
  313. }
  314. }
  315. };
  316.  
  317.  
  318. // Create an observer instance linked to the callback function
  319. const observer = new MutationObserver(callback);
  320.  
  321. // Start observing the target node for configured mutations
  322. console.log("Jobs List element", jobsList);
  323. observer.observe(jobsList, config);
  324. }());
  325.