LinkedIn Job Search Usability Improvements

Make the interface easier to use

当前为 2020-01-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name LinkedIn Job Search Usability Improvements
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2
  5. // @description Make the interface easier to use
  6. // @author Bryan Chan
  7. // @match http://www.linkedin.com/jobs/search/*
  8. // @grant GM_addStyle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. console.log("Starting LinkedIn Job Search Usability Improvements");
  17.  
  18. // Setup dictionaries to persist useful information across sessions
  19. class StoredDictionary {
  20. constructor(storageKey) {
  21. this.storageKey = storageKey;
  22. this.data = GM_getValue(storageKey) || {};
  23. console.log("Initial data read from", this.storageKey, this.data);
  24. }
  25.  
  26. get(key) {
  27. return this.data[key];
  28. }
  29.  
  30. set(key, value) {
  31. this.data[key] = value;
  32. GM_setValue(this.storageKey, this.data);
  33. console.log("Updated data", this.storageKey, this.data);
  34. }
  35.  
  36. getDictionary() {
  37. return this.data;
  38. }
  39. }
  40.  
  41. const hiddenCompanies = new StoredDictionary("hidden_companies");
  42. const hiddenPosts = new StoredDictionary("hidden_posts");
  43. const readPosts = new StoredDictionary("read_posts");
  44.  
  45. /** Install key handlers to allow for keyboard interactions */
  46. const KEY_HANDLER = {
  47. "e": handleMarkRead,
  48. "j": goToNext,
  49. "k": goToPrevious,
  50. "x": handleHidePost,
  51. "y": handleHideCompany,
  52. "?": handlePrintDebug,
  53. }
  54.  
  55. window.addEventListener("keydown", function(e) {
  56. const handler = KEY_HANDLER[e.key]
  57. if(handler) handler();
  58. });
  59.  
  60. /** Event handler functions */
  61. const FEEDBACK_DELAY = 300;
  62.  
  63. // Handle a request to hide a post forever
  64. function handleHidePost() {
  65. const activeJob = getActive();
  66. const data = getCardData(activeJob);
  67.  
  68. const postTitle = getPostNode(activeJob);
  69. postTitle.style.textDecoration = "line-through";
  70.  
  71. setTimeout(() => {
  72. goToNext();
  73. hiddenPosts.set(data.postUrl, `${data.companyName}: ${data.postTitle}`);
  74. updateDisplay();
  75. }, FEEDBACK_DELAY);
  76. }
  77.  
  78. // Handle request to hide all posts from a company, forever
  79. function handleHideCompany() {
  80. const activeJob = getActive();
  81. const data = getCardData(activeJob);
  82.  
  83. // show feedback
  84. const company = getCompanyNode(activeJob);
  85. company.style.textDecoration = "line-through";
  86.  
  87. setTimeout(() => {
  88. // go to next post and hide the company
  89. goToNext();
  90. hiddenCompanies.set(data.companyUrl, data.companyName);
  91. updateDisplay();
  92. }, FEEDBACK_DELAY);
  93. }
  94.  
  95. // Handl request to mark a post as read (
  96. function handleMarkRead() {
  97. // @TODO implement this in a useful way
  98. const activeJob = getActive();
  99. const data = getCardData(activeJob);
  100. goToNext();
  101. readPosts.set(data.postUrl, `${data.companyName}: ${data.postTitle}`);
  102. updateDisplay();
  103. }
  104.  
  105. // Handle requests to print debug information
  106. function handlePrintDebug() {
  107.  
  108. console.log("Hidden companies");
  109. console.log(hiddenCompanies.getDictionary());
  110.  
  111. console.log("Hidden posts");
  112. console.log(hiddenPosts.getDictionary());
  113.  
  114. console.log("Read posts");
  115. console.log(readPosts.getDictionary());
  116. }
  117.  
  118. /** Functions to adjust jobs list display, based on which companies, posts are hidden and which posts are read */
  119. const jobsList = document.querySelector("ul.jobs-search-results__list");
  120. var updateQueued = false;
  121. var updateTimer = null;
  122. function queueUpdate() {
  123. if(updateTimer) {
  124. clearTimeout(updateTimer);
  125. }
  126. updateTimer = setTimeout(function() {
  127. updateTimer = null;
  128. updateDisplay()
  129. }, 30);
  130. }
  131. function updateDisplay() {
  132. console.log("Updating display on jobs list");
  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. //console.log("Updating display on", data);
  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 in", elapsed);
  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(isHidden(next.firstElementChild)) {
  178. next = next.nextSibling.nextSibling;
  179. }
  180. triggerClick(next.firstElementChild);
  181. } else {
  182. goToFirst();
  183. }
  184. }
  185.  
  186. function goToPrevious() {
  187. const active = getActive();
  188. if(active) {
  189. var prev = active.previousSibling.previousSibling;
  190. while(isHidden(prev.firstElementChild)) {
  191. prev = prev.previousSibling.previousSibling;
  192. }
  193. triggerClick(prev.firstElementChild);
  194. } else {
  195. goToFirst();
  196. }
  197. }
  198.  
  199. function triggerClick (node) {
  200. triggerMouseEvent (node, "mouseover");
  201. triggerMouseEvent (node, "mousedown");
  202. triggerMouseEvent (node, "mouseup");
  203. triggerMouseEvent (node, "click");
  204. }
  205.  
  206. /** Check if a card is hidden */
  207. function isHidden (node) {
  208. return node.classList.contains("jobs-search-results-feedback") ||
  209. node.classList.contains("hidden");
  210. }
  211.  
  212. /** Extracts card data from a card */
  213. function getCompanyNode (node) {
  214. return node.querySelector("a.job-card-search__company-name-link")
  215. }
  216. function getPostNode (node) {
  217. return node.querySelector(".job-card-search__title a.job-card-search__link-wrapper")
  218. }
  219. function getCardData (node) {
  220. const company = getCompanyNode(node);
  221. const companyUrl = company.getAttribute("href");
  222. const companyName = company.text.trim(" ");
  223. const post = getPostNode(node);
  224. const postUrl = post.getAttribute("href").split("/?")[0];
  225. const postTitle = post.text.replace("Promoted","").trim(" \n");
  226. return {
  227. companyUrl,
  228. companyName,
  229. postUrl,
  230. postTitle
  231. };
  232. }
  233.  
  234. GM_addStyle(".jobs-search-results-feedback { display: none }");
  235. GM_addStyle(".hidden { display: none }");
  236. GM_addStyle(".read { opacity: 0.3 }");
  237.  
  238.  
  239. console.log("Adding mutation observer");
  240.  
  241. // Options for the observer (which mutations to observe)
  242. const config = { attributes: true, childList: true, subtree: true };
  243.  
  244. // Callback function to execute when mutations are observed
  245. const callback = function(mutationsList, observer) {
  246. console.log("Mutation!");
  247. // Use traditional 'for loops' for IE 11
  248. for(let mutation of mutationsList) {
  249. const target = mutation.target;
  250. if (mutation.type === 'childList') {
  251. console.log('Children were modified on', target);
  252. queueUpdate();
  253. }
  254. else if (mutation.type === 'attributes') {
  255. //console.log('The ' + mutation.attributeName + ' attribute was modified.', target);
  256. }
  257. }
  258. };
  259.  
  260.  
  261. // Create an observer instance linked to the callback function
  262. const observer = new MutationObserver(callback);
  263.  
  264. // Start observing the target node for configured mutations
  265. console.log("Jobs List element", jobsList);
  266. observer.observe(jobsList, config);
  267. }());
  268.