// -*- coding:utf-8; -*- // ==UserScript== // @name UpworkTown // @description Town and city filter for Upwork job searches // @version 0.1 // @author mike@reluk.ca // @namespace reluk.ca // @include /^https?://(?:www\.)?upwork\.com// // @run-at document-idle // @resource stylesheet http://reluk.ca/var/cache/userscript/UpworkTown.css // @grant GM_addStyle // @grant GM_getResourceText // ==/UserScript== // // Overview // -------- // UpworkTown is a userscript that helps you find jobs in your hometown. The Upwork site itself // doesn't support filtering a job search by town or city, so it has to be done manually. But // UpworkTown can make it a little easier. // // See also: https://community.upwork.com/t5/Freelancers/Filter-job-search-by-city/m-p/286066 // // Requirements // ------------ // Tested browsers are Firefox 50 with Greasemonkey, and Chrome 54 with Tampermonkey. // // It might work with other setups, I can't say for sure. // // Instructions // ------------ // 1. Install the userscript in your web browser. // // Paste its URL in your address bar: http://reluk.ca/var/cache/userscript/UpworkTown.user.js // // 2. Set the name of your hometown. // // Edit the installed userscript and change the variable called "townName". // // 3. Ensure you're logged in to Upwork. // // The page structure differs when you're logged into the site, and the userscript depends on this. // // 4. Search for jobs in your home country. In my case, for example, the URL might be: // https://www.upwork.com/o/jobs/browse/?location=Canada // // Narrow the results as much as possible using the filters in the side bar. // // 5. Open all the new (unclicked) jobs in background tabs. // // The unclicked ones are coloured differently by the browser. Click on each with the mouse wheel. // Keep clicking and let the tabs line up in the background. // // 6. Delete the tabs named "out". // // UpworkTown retitles the tabs "in" or "out" depending on client location. That way you don't // have to view out-of-town jobs at all (foreground them), but can just delete them all unseen. // // 7. Open the remaining tabs, which are named "in". Those are the hometown jobs. // // Support // ------- // I don't promise anything, but if you post a job on Upwork and invite me, then I might be able to // customize this userscript (or code another one for you) in return for a token fee and a rating on // the site. My user page there is: https://www.upwork.com/fl/michaelallan ( 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 ));