// -*- coding:utf-8; -*- // ==UserScript== // @name Upwork // @description Transforms the Upwork site to suit me // @author mike@reluk.ca // @namespace reluk.ca // @include /^https?://(?:www\.)?upwork\.com// // @run-at document-idle // @resource stylesheet http://reluk.ca/var/cache/userscript/Upwork.css // @grant GM_addStyle // @grant GM_getResourceText // ==/UserScript== /** [ making skills visible - in job search results ( data-ng-app FindWorkHomeUI | JobSearchUI - by default, without clicking *more* to expand the description of each job posting - can't do it consistently - somehow the FindWorkHomeUI DOM hasn't the relevant data < it *is* in the JobSearchUI, deeply embedded somewhere outside the job posting */ ( function( body ) { /** Appends one or more style classes to element e. * * @param newNames (String) The names of the new style classes to append, separated by spaces. */ function appendStyleClass( newNames, e ) { var oldNames = e.className; e.className = oldNames? oldNames + ' ' + newNames: newNames; } /** Intitializes this userscript. */ function init() { // console.log( 'UpworkTown userscript' ); // TEST switch( body.getAttribute( 'data-ng-app' )) { case 'FindWorkHomeUI': // e.g. https://www.upwork.com/ab/find-work/ toRestartOnFeed = true; transformSearchResults_start(); break; case 'jobDetails': GM_addStyle( GM_getResourceText( 'stylesheet' )); // from @resource scheduledTry = setTimeout( transformJobPost, msTryDelay = 100 ); break; case 'JobSearchUI': // e.g. https://www.upwork.com/o/jobs/browse/?location=Canada // ( function( pushStateWas ) // credit Earnshaw: http://stackoverflow.com/a/7381436/2402790 // { // history.pushState = function() // { // pushStateWas.apply( window.history, arguments ); // console.log( 'pushed location.href=' + location.href ); // TEST // } // })( history.pushState ); /// fails to detect Upwork's paging, as does replaceState equivalent, ∴ must poll instead: var locWas = null; setInterval( function() // (re)start the transformation process for each page of results { var loc = location.href; if( loc == locWas ) return; // page unchanged locWas = loc; transformSearchResults_start(); // restart }, 100/*ms*/ ); // break; } } /** Duration of pause before the next transformation attempt. */ var msTryDelay; /** The currently scheduled transformation attempt, or null if none is scheduled. */ var scheduledTry = null; /** Whether to restart the transformation process on detecting that new search results have been fed * into the page. * * @see #willRestartOnFeed */ var toRestartOnFeed = false; /** The name of the town or city whose jobs the user wants to see. */ var townName = 'Toronto'; /** Tranforms a present document that contains a job posting. */ function transformJobPost() { scheduledTry = null; var anchorSection = document.getElementById( 'jobsProviderAction' ); if( !anchorSection ) { // It's likely still pending. The document source encodes the town but no anchor to orient // by. It's introduced later and the document restructured at that time. Wait for it. scheduledTry = setTimeout( transformJobPost, msTryDelay += 500 ); // += avoids over-polling return; } var walker = document.createTreeWalker( anchorSection.parentNode, NodeFilter.SHOW_TEXT, null, false ); if( !walker ) return; var townText = null; for( ;; ) { var text = walker.nextNode(); if( !text ) break; if( text.data.indexOf(townName) < 0 ) continue; townText = text; break; } var titlePrefix; if( townText ) { titlePrefix = 'in: '; appendStyleClass( 'inTown', townText.parentNode.parentNode ); } else titlePrefix = 'out: '; document.title = titlePrefix + document.title; // console.log( 'final msTryDelay=' + msTryDelay ); // TEST } /** Attempts to tranform a present document that contains the results of a job search, rescheduling * a further attempt if it detects failure. */ function transformSearchResults() { scheduledTry = null; var walker = document.createTreeWalker( body, NodeFilter.SHOW_ELEMENT, null, false ); if( !walker ) return; var xCount = 0; // transformation count for( ;; ) { // Make each job posting link open in a new context (tab or window). This isn't needed if // the user remembers to click with the mouse wheel, but I often forget. So I rely on this // transformation, plus Firefox about:config setting browser.tabs.loadInBackground, as per // https://support.mozilla.org/en-US/questions/1054179. var e = walker.nextNode(); if( !e ) break; if( e.localName != 'a' ) continue; // not a link if( e.target == '_blank' ) continue; // nothing to transform, avoid falsifying xCount var href = e.href; if( !href || href.indexOf('upwork.com/jobs/') < 0 ) continue; // not a job posting link e.target = '_blank'; // make it open in a new context, e.g. tab or window ++xCount; } if( toRestartOnFeed && !willRestartOnFeed ) // testing late, when feedDiv may finally exist { var feedDiv = document.getElementById( 'feed-jobs' ); if( feedDiv ) { new MutationObserver( function( mutations ) { transformSearchResults_start(); // restart }).observe( feedDiv, { childList: true } ); willRestartOnFeed = true; } } if( xCount < 4 ) // then search results are likely still pending { scheduledTry = setTimeout( transformSearchResults, msTryDelay += 500 ); // retry after a pause, longer each time to avoid over-polling return; } // console.log( 'final msTryDelay=' + msTryDelay ); // TEST } /** Starts or restarts the transformation process. */ function transformSearchResults_start() { if( scheduledTry ) clearTimeout( scheduledTry ); scheduledTry = setTimeout( transformSearchResults, msTryDelay = 100 ); } /** Whether the transformation process will actually be restarted on detecting that new search * results have been fed into the page. * * @see #toRestartOnFeed */ var willRestartOnFeed = false; //////////////////// init(); }( document.body )); // Note to myself: In order to ease maintenance, my Greasemonkey/Firefox installation of this userscript // was linked back to the original source files by the following command: // (n=UpworkTown; ln --force --symbolic ~/var/cache/userscript/$n.user.js ~/.mozilla/firefox/ikouvatt.default/gm_scripts/$n/)