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-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name LinkedIn Job Search Usability Improvements
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.4
  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. "x": handleHidePost, // hide post forever
  53. "y": handleHideCompany, // hide company forever
  54. "?": handlePrintDebug, // print debug information to the console
  55. }
  56.  
  57. window.addEventListener("keydown", function(e) {
  58. const handler = KEY_HANDLER[e.key]
  59. if(handler) handler();
  60. });
  61.  
  62. /** Event handler functions */
  63. const FEEDBACK_DELAY = 300;
  64.  
  65. // Toggle whether to hide posts
  66. var showHidden = false;
  67. function toggleHidden() {
  68. showHidden = !showHidden;
  69. queueUpdate();
  70. }
  71.  
  72. // Handle a request to hide a post forever
  73. function handleHidePost() {
  74. const activeJob = getActive();
  75. const data = getCardData(activeJob);
  76.  
  77. // Show feedback
  78. activeJob.style.opacity = 0.6;
  79. const postTitle = getPostNode(activeJob);
  80. postTitle.style.textDecoration = "line-through";
  81.  
  82. const detailPostTitle = document.querySelector(".jobs-details-top-card__job-title");
  83. detailPostTitle.style.textDecoration = "line-through";
  84.  
  85. // Wait a little and then hide post
  86. setTimeout(() => {
  87. goToNext();
  88. detailPostTitle.style.textDecoration = "none";
  89. hiddenPosts.set(data.postUrl, `${data.companyName}: ${data.postTitle}`);
  90. updateDisplay();
  91. }, FEEDBACK_DELAY);
  92. }
  93.  
  94. // Handle request to hide all posts from a company, forever
  95. function handleHideCompany() {
  96. const activeJob = getActive();
  97. const data = getCardData(activeJob);
  98.  
  99. // show feedback
  100. activeJob.style.opacity = 0.6;
  101. const company = getCompanyNode(activeJob);
  102. company.style.textDecoration = "line-through";
  103.  
  104. const detailCompany = document.querySelector(".jobs-details-top-card__company-url");
  105. detailCompany.style.textDecoration = "line-through";
  106.  
  107. // Wait a little and then hide company
  108. setTimeout(() => {
  109. // go to next post and hide the company
  110. goToNext();
  111. detailCompany.style.textDecoration = "none";
  112. hiddenCompanies.set(data.companyUrl, data.companyName);
  113. updateDisplay();
  114. }, FEEDBACK_DELAY);
  115. }
  116.  
  117. // Handl request to mark a post as read (
  118. function handleMarkRead() {
  119. // @TODO implement this in a useful way
  120. const activeJob = getActive();
  121. const data = getCardData(activeJob);
  122. goToNext();
  123. readPosts.set(data.postUrl, `${data.companyName}: ${data.postTitle}`);
  124. updateDisplay();
  125. }
  126.  
  127. // Handle requests to print debug information
  128. function handlePrintDebug() {
  129.  
  130. console.log("Hidden companies");
  131. console.log(hiddenCompanies.getDictionary());
  132.  
  133. console.log("Hidden posts");
  134. console.log(hiddenPosts.getDictionary());
  135.  
  136. console.log("Read posts");
  137. console.log(readPosts.getDictionary());
  138. }
  139.  
  140. /** Functions to adjust jobs list display, based on which companies, posts are hidden and which posts are read */
  141. const jobsList = document.querySelector("ul.jobs-search-results__list");
  142. var updateQueued = false;
  143. var updateTimer = null;
  144. function queueUpdate() {
  145. if(updateTimer) {
  146. clearTimeout(updateTimer);
  147. }
  148. updateTimer = setTimeout(function() {
  149. updateTimer = null;
  150. updateDisplay()
  151. }, 30);
  152. }
  153. function updateDisplay() {
  154. const start = +new Date();
  155. for(var job = jobsList.firstElementChild; job.nextSibling; job = job.nextSibling.nextSibling) {
  156. try {
  157. const data = getCardData(job);
  158. const jobDiv = job.firstElementChild;
  159.  
  160. if(showHidden) {
  161. jobDiv.classList.remove("hidden");
  162. continue;
  163. }
  164.  
  165. if(hiddenCompanies.get(data.companyUrl)) {
  166. jobDiv.classList.add("hidden");
  167. } else if(hiddenPosts.get(data.postUrl)) {
  168. jobDiv.classList.add("hidden");
  169. } else if(readPosts.get(data.postUrl)) {
  170. jobDiv.classList.add("read");
  171. }
  172.  
  173. } catch(e) {
  174. }
  175. }
  176. const elapsed = +new Date() - start;
  177. console.log("Updated display on jobs list in", elapsed, "ms");
  178. }
  179.  
  180. function triggerMouseEvent (node, eventType) {
  181. var clickEvent = document.createEvent ('MouseEvents');
  182. clickEvent.initEvent (eventType, true, true);
  183. node.dispatchEvent (clickEvent);
  184. }
  185.  
  186. /** Get active job card */
  187. function getActive() {
  188. const active = document.querySelector(".job-card-search--is-active");
  189. return active ? active.parentNode : undefined;
  190. }
  191.  
  192. /** Select first card in the list */
  193. function goToFirst() {
  194. const firstPost = jobsList.firstElementChild;
  195. const clickableDiv = firstPost.firstElementChild;
  196. triggerClick(clickableDiv);
  197. }
  198.  
  199. function goToNext() {
  200. const active = getActive();
  201. if(active) {
  202. var next = active.nextSibling.nextSibling;
  203. while(next.firstElementChild && isHidden(next.firstElementChild)) {
  204. next = next.nextSibling.nextSibling;
  205. }
  206. if(next.firstElementChild) {
  207. triggerClick(next.firstElementChild);
  208. }
  209. } else {
  210. goToFirst();
  211. }
  212. }
  213.  
  214. function goToPrevious() {
  215. const active = getActive();
  216. if(active) {
  217. var prev = active.previousSibling.previousSibling;
  218. while(prev.firstElementChild && isHidden(prev.firstElementChild)) {
  219. prev = prev.previousSibling.previousSibling;
  220. }
  221. if(prev.firstElementChild) {
  222. triggerClick(prev.firstElementChild);
  223. }
  224. } else {
  225. goToFirst();
  226. }
  227. }
  228.  
  229. function triggerClick (node) {
  230. triggerMouseEvent (node, "mouseover");
  231. triggerMouseEvent (node, "mousedown");
  232. triggerMouseEvent (node, "mouseup");
  233. triggerMouseEvent (node, "click");
  234. }
  235.  
  236. /** Check if a card is hidden */
  237. function isHidden (node) {
  238. return node.classList.contains("jobs-search-results-feedback") ||
  239. node.classList.contains("hidden");
  240. }
  241.  
  242. /** Extracts card data from a card */
  243. function getCompanyNode (node) {
  244. return node.querySelector("a.job-card-search__company-name-link")
  245. }
  246. function getPostNode (node) {
  247. return node.querySelector(".job-card-search__title a.job-card-search__link-wrapper")
  248. }
  249. function getCardData (node) {
  250. var companyUrl, companyName, postUrl, postTitle;
  251. const company = getCompanyNode(node);
  252. if(company) {
  253. companyUrl = company.getAttribute("href");
  254. companyName = company.text.trim(" ");
  255. }
  256.  
  257. const post = getPostNode(node);
  258. if(post) {
  259. postUrl = post.getAttribute("href").split("/?")[0];
  260. postTitle = post.text.replace("Promoted","").trim(" \n");
  261. }
  262. return {
  263. companyUrl,
  264. companyName,
  265. postUrl,
  266. postTitle
  267. };
  268. }
  269.  
  270. /** Add styles to handle hiding */
  271. GM_addStyle(".jobs-search-results-feedback { display: none }");
  272. GM_addStyle(".hidden { display: none }");
  273. GM_addStyle(".read { opacity: 0.3 }");
  274.  
  275.  
  276. console.log("Adding mutation observer");
  277.  
  278. // Options for the observer (which mutations to observe)
  279. const config = { attributes: true, childList: true, subtree: true };
  280.  
  281. // Callback function to execute when mutations are observed
  282. const callback = function(mutationsList, observer) {
  283. // Use traditional 'for loops' for IE 11
  284. for(let mutation of mutationsList) {
  285. const target = mutation.target;
  286. if (mutation.type === 'childList') {
  287. queueUpdate();
  288. }
  289. else if (mutation.type === 'attributes') {
  290. //console.log('The ' + mutation.attributeName + ' attribute was modified.', target);
  291. }
  292. }
  293. };
  294.  
  295.  
  296. // Create an observer instance linked to the callback function
  297. const observer = new MutationObserver(callback);
  298.  
  299. // Start observing the target node for configured mutations
  300. console.log("Jobs List element", jobsList);
  301. observer.observe(jobsList, config);
  302. }());
  303.