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