Related Links Pager

Navigate sideways! When you click a link, related links on the current page are carried with you. They can be accessed from a pager on the target page, so you won't have to go back in your browser.

目前為 2015-09-09 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Related Links Pager
  3. // @namespace RLP
  4. // @description Navigate sideways! When you click a link, related links on the current page are carried with you. They can be accessed from a pager on the target page, so you won't have to go back in your browser.
  5. // @version 1.2.3
  6. // @downstreamURL http://userscripts.org/scripts/source/124293.user.js
  7. // @include http://*/*
  8. // @include https://*/*
  9. // @exclude http://www.facebook.com/*
  10. // @exclude https://www.facebook.com/*
  11. // @exclude http://twitter.com/*
  12. // @exclude https://twitter.com/*
  13. // @exclude https://*.xmarks.com/*
  14. // @no-longer-exclude http://github.com/*
  15. // @no-longer-exclude https://github.com/*
  16. // @exclude https://chrome.google.com/webstore/
  17. // @grant GM_addStyle
  18. // @grant GM_log
  19. // @grant GM_setValue
  20. // @grant GM_getValue
  21. // ==/UserScript==
  22.  
  23.  
  24.  
  25. // Google redirection can block #siblings from being carried to the target page. If that happens, this script may help: http://userscripts.org/scripts/show/121261#Straight_Google
  26.  
  27. // DONE: Using the pager always sets the siblings packet in the target URL, even if passPacketByGM is enabled! Fix that.
  28. // FIXED: Still problems with the pager not working when passPacketByGM is enabled. For some reason the pager shows us focused on the wrong link in the list! (Was browsing vim.org, URLs_Need_Titles was adding #s to the title but not the links.) Also, sometimes the old packet was getting cleared and a new one was not loading when clicking pager clicks.
  29. // DONE: We want RLP to run on github, for lists of external links, just not for links to local pages (or it can always run if passPacketByGM!). Same could be said for twitter, facebook, etc.
  30. // CONSIDER: When following same-domain links, Chrome could opt to use GM_set/get or localStorage, rather than the messy #siblings packet.
  31.  
  32. // TODO: RLP does not fire for sites which use HTML5 History API to "change" page. We could detect use of push/replaceState, and rebuild the pager after a timeout or event.
  33.  
  34. // BUG: Breaks digitalocean community tutorials, when reached via google search.
  35.  
  36.  
  37.  
  38. // == OPTIONS ==
  39.  
  40. var delayBeforeRunning = 2000;
  41. var minimumGroupSize = 2;
  42. var maximumGroupSize = 250; // Some webservers restrict long URLs, responding with "Bad Request".
  43. var groupLinksByClass = true;
  44. if (document.location.href.match(/google.*(search|q=)/)) {
  45. groupLinksByClass = false; // Most Google results have class "l" but any previously visited have "l vst".
  46. }
  47. // CONSIDER TODO: A better compromise for all sites might be groupLinksWhichShareAtLeastOneClass. This would reject links which do not share any classes with the focused link.
  48.  
  49. var showGroupCountInLinkTitle = true; // Updates hovered links' titles to show number of siblings.
  50. var showPageNumberInWindowTitle = false; // Updates title of current page to show current "page" number.
  51.  
  52. var enableOnCtrlClick = true;
  53. var enableOnShiftClick = true; // Allows you to avoid the script when needed
  54. var enableOnRightClick = false;
  55.  
  56. var keepNavigationHistory = false; // When off, sideways paging is not added to the browser history. The back button will return you to the page before you started paging, not the previous page you were on.
  57. var leaveHashUrlsAlone = true; // Many sites use # these days for their own purposes - this avoids the risk of breaking them.
  58. var forceTravel = false; // Attempt to fix loss of #data when clicking thumbnails on YouTube. Failed to fix it!
  59. // BUG: When attempting to open a link in a new tab with Ctrl-click, forceTravel will make the current tab navigate to that page.
  60. var clearDataFromLocation = true; // Tidies up your location bar URL, but prevents the pager from re-appearing when navigating Back to this page (or reloading it) - OR adds an extra step to history, depending on the implementation chosen below. Disable this for debugging.
  61.  
  62. var highlightLinkGroups = true; // Change the background or border of links in the current group on hover.
  63. var changeBackgroundNotBorder = true; // If false, draws boxes around related links. (Then you may want to increase the opacity of the colors below.)
  64. var thisLinkHighlightColor = "rgba(130,200,255,0.1)"; // very light blue
  65. var highlightColor = "rgba(130,200,255,0.2)"; // light blue
  66. // var thisLinkHighlightColor = "rgba(0,255,255,0.01)"; // very faint cyan
  67. // var highlightColor = "rgba(0,255,255,0.08)"; // faint cyan
  68. // var thisLinkHighlightColor = null;
  69. // var highlightColor = "rgba(130,230,255,0.3)";
  70. // var thisLinkHighlightColor = "rgba(0,0,255,0.1)"; // very faint blue
  71. // var highlightColor = "rgba(0,0,255,0.2)"; // faint blue
  72. var visitingColor = "rgba(220,130,255,0.2)"; // light purple
  73. var lineStyle = "solid";
  74. // var lineStyle = "dashed";
  75.  
  76. var useLocalStorageWhenPossible = true; // Hide #siblings from URL when we are travelling to a page on the same host. Pass data by localStorage instead (implemented through fake GM_set). This replaces the old passPacketByGM. The only disadvantage is that going *back* to a non-#siblings URL will lose the pager that was previously there.
  77.  
  78. var beFrugal = false; // When forced to use #siblings, only do so on Google search results pages.
  79.  
  80. var verbose = false; // Extra logging for debugging
  81.  
  82.  
  83.  
  84. // == CHANGELOG ==
  85. // 2012-10-27 Added passPacketByGM for all browsers except Chrome.
  86. // 2012-10-21 Fixes for Google search results link rewriting war!
  87. // 2012-10-08 Fixed inefficiencies in getXPath which could cause lockups.
  88. // 2012-03-22 Added highlighting and further heuristics.
  89. // 2012-02-07 Bugfixes and more heuristics.
  90. // 2012-01-30 Fixed repeat rewrite bug by checking for &sib as well as #sib.
  91. // 2012-01-28 Fixed close button positioning in Firefox.
  92. // 2012-01-28 Restricted related links to those with the same CSS class.
  93. // BUG: We still group unrelated (indistinguishable) links on Wikipedia!
  94. // 2012-01-28 Blacklisted some buggy situations.
  95.  
  96.  
  97.  
  98. // == NOTES ==
  99.  
  100. // FIXED: On huge pages this used to run very slowly, locking up Firefox.
  101. // This was due to an inefficient for loop in getXPath.
  102. // DONE: This can be easily fixed, since we don't actually use the numbers!
  103.  
  104. // You can avoid this script either by clicking a link before it runs or by
  105. // activating the desired link with the keyboard instead of the mouse.
  106.  
  107. // TODO: The # method seems to work but could present a BUG on some sites. It
  108. // is sometimes needed but it's horrible. Use alternatives where possible. In
  109. // Chrome that could be localStorage *if* the followed sibling is on the same
  110. // domain. In Firefox Greasemonkey we can use GM_set/getValue().
  111.  
  112. // TODO: Pager can only go sideways. It could also offer ability to go "up"
  113. // to the page that contained all the links in the current group.
  114.  
  115. // BUG: Does not work through redirects.
  116.  
  117. // CONSIDER: Would it be better to use a normal CGI '&' instead of '#' ?
  118. // Should we compress the data to survive through more webservers?
  119.  
  120. // TODO: Do not load pager if it is redundant (i.e. if we can already see the
  121. // sibling links on the current page!).
  122.  
  123. // DONE: removeRedirection only runs on links originally in the page, not
  124. // those added later, e.g. by Ajax when refining a Google search. This may be
  125. // the cause of some issues. (Google says "This url is too long.")
  126.  
  127. // FIXED: Now we are passing ,1 to mark the "current" page, but this is
  128. // being passed into the siblings package. The package needs to be rebuilt and
  129. // altered! That was a fairly rare case: redirected to a new domain but
  130. // siblings retained!
  131.  
  132. // BUG TODO: With forceTravel=true, submitting an answer on StackOverflow, we
  133. // get a warning message that we are about to leave the current page! (In fact
  134. // is was fine for me to accept the warning and the post happened ok, but still
  135. // hardly tidy!)
  136. //
  137. // Recommend: Default to forceTravel=false, and override with heuristics only
  138. // for Google search! So it works gently for all users, never causing
  139. // problems, but sometimes failing(being invisibly overriden - gah!). Enable
  140. // it for personal use, as other powerusers might do.
  141. //
  142. // Another place our script is overzealous is on StackOverflow, when clicking
  143. // to expand a post, AJAX works just fine, but forceTravel sends us to a
  144. // different page regardless! Solution: don't click on the link, click on the
  145. // div.
  146. //
  147. // I can't see any way how forceTravel could detect whether the user wants it
  148. // or not. Well, the user wants it only if clicking would take them to a new
  149. // page. Could we perhaps catch the page change event and if it happened
  150. // immediately after a click, ensure the target URL has siblings packet. I'm
  151. // not sure JS has the power to do that through onbeforeunload.
  152. //
  153. // Perhaps we should go for a medium option - remove .onclick and unregister
  154. // any event handlers (we can't do that!). Sites *should* fall back to their
  155. // non-JS method.
  156. //
  157. // Oh well we already are removing onmousedown, perhaps we should do click and
  158. // mouseup also, but make them all optional.
  159.  
  160. // TODO: Appending # data breaks Twitter. @exclude is not the solution. We
  161. // should just not append to links who land on twitter (from in or out), whilst
  162. // links leaving Twitter should be fine!
  163.  
  164. // TODO: Despite setting forceTravel, Google search results pages sometimes
  165. // send us to their own click-tracking URL which redirects us to the target
  166. // page but loses our siblings packet. One solution to this might be to
  167. // replace the link in the page with our own A element, so that clicking it
  168. // will not fire directly linked events. (It could still however trigger
  169. // events attached to a parent. Is it possible to override/prevent them with
  170. // an event listener we add later?)
  171.  
  172.  
  173.  
  174. // passPacketByGM is now auto-determined at click time
  175. /*
  176. var passPacketByGM = false;
  177. if (typeof GM_setValue == 'function') {
  178. passPacketByGM = true; // It is safe to disable this if you want the old behaviour
  179. // In Chrome though, GM_setValue is not cross-domain, which we need!
  180. if (window.navigator.vendor.match(/Google/)) {
  181. passPacketByGM = false;
  182. }
  183. // FBGMAPI check. We cannot pass by GM if it is domain-restricted.
  184. // (TODO: Actually we could, if the target was local too.)
  185. if (GM_setValue.restrictedToDomain) {
  186. passPacketByGM = false;
  187. }
  188. // TODO: We may want more specific checks here. The presence of
  189. // GM_setValue does not indicate that it will work cross-domain. It would be
  190. // safer to use the # fallback unless we are *sure* we have a XD GMsV.
  191. }
  192. */
  193. if (window.navigator.vendor.match(/Google/) || typeof GM_setValue != 'function') {
  194. GM_setValue = function(key, val){
  195. localStorage["RLP_Fake_GM:"+key] = val;
  196. };
  197. GM_getValue = function(key) {
  198. return localStorage["RLP_Fake_GM:"+key];
  199. };
  200. }
  201.  
  202.  
  203.  
  204. // We grab the data as early as possible, in case any other scripts decide to
  205. // change the #. We delay running anything else for a little while.
  206. var grabbedList;
  207. var clearDataTimer = null;
  208. if (document.location.hash && document.location.hash.indexOf("siblings=")>=0) {
  209. grabbedList = document.location.hash.replace(/.*[#&]siblings=([^#]*)/,'$1');
  210. if (grabbedList) {
  211. grabbedList = decodeURIComponent(grabbedList);
  212. }
  213. }
  214. if (!grabbedList) {
  215. grabbedList = GM_getValue("siblings_data");
  216. // GM_log("[RLP] Got siblings_data="+grabbedList);
  217. // I often see this logged twice, and if we cleanse the siblings_data immediately, no pager appears. This could be caused by google redirection, or perhaps even by an iframe which loads faster than the page.
  218. // Let's delay the cleansing (by a full 15 seconds for modem users).
  219. // The reason we want to cleanse is when the user later visits pages without the pager (e.g. from a bookmark), then that page should not pick up the packet! We *could* address that by checking whether we are one of the targets in the packet, but we very occasionally had issues with that check, so opted to always display the pager if we have data
  220. clearDataTimer = setTimeout(function(){
  221. var grabbedListNow = GM_getValue("siblings_data");
  222. if (grabbedListNow == grabbedList) {
  223. GM_setValue("siblings_data","");
  224. }
  225. clearDataTimer = null;
  226. },15000);
  227. // The passPacketByGM approach loses the earlier feature of retaining the pager if we come Back to this page.
  228. }
  229.  
  230. function onAGoogleSearchPage() {
  231. // return document.location.hostname.indexOf("google")>=0 && document.location.href.indexOf("search")>=0;
  232. return document.location.hostname.indexOf("google")>=0 && document.location.href.match(/\bq=/);
  233. }
  234. // CHECK_IF_GOOGLE
  235. // This is heavy-handed and didn't even work. stopPropagation did.
  236. /*if (onAGoogleSearchPage()) {
  237. forceTravel = true; // Since removeAttribute("onmousedown") stopped working
  238. groupLinksByClass = false; // Most links get class "l" but some get class "l vst" (previously visited)
  239. }*/
  240. // Dear Google: I don't mind giving you useful feedback about which links I
  241. // clicked, but I *need* my siblings packet in the final arrival URL!
  242.  
  243. // Occasionally (when a web page has no title) the window will get the URL as its title. If Related_Links_Pager has created a *very* long URL, this can be upsetting to window managers. (Specifically it was slowing down Fluxbox, although they have fixed that bug now.) Avoid that potential issue by restricting the title's length.
  244. if (document.title.length==0 && document.location.href.length>800) {
  245. document.title = document.location.href.slice(0,100) + " ...";
  246. }
  247. // Let's also fix it, even if it wasn't our fault
  248. if (document.title.length > 800) {
  249. document.title = document.title.slice(0,100) + " ...";
  250. }
  251.  
  252.  
  253.  
  254. function runRelatedLinksPager(){
  255.  
  256.  
  257.  
  258. // Library functions
  259.  
  260. function getXPath(node) {
  261. if (!node) {
  262. return '';
  263. }
  264. return getXPath(node.parentNode) + '/' + node.nodeName.toLowerCase();
  265. }
  266.  
  267. // What can I say? I loooove favicons!
  268. // BUG: Does not add a favicon for the current page, because the current page
  269. // is not shown as a link. This breaks left-alignment of the text!
  270. function addFaviconToLinkObviouslyIMeanWhyWouldntYou(link) {
  271.  
  272. if (!link.href) {
  273. return;
  274. }
  275.  
  276. var host = link.href.replace(/^[^\/]*:\/\//,'').replace(/\/.*$/,'');
  277. var img = document.createElement('IMG');
  278. // img.src = 'http://'+host+'/favicon.ico';
  279.  
  280. var alwaysUseGoogle = false;
  281. var imageExtensions = ( alwaysUseGoogle ? [] : ['gif','jpg','png','ico'] );
  282. function tryExtension(evt) {
  283. var ext = imageExtensions.pop();
  284. // Use protocol (http/https) of current page, to avoid mixed-content warnings/failures.
  285. var protocol = document.location.protocol.replace(/:$/, '');
  286. if (ext) {
  287. img.src = protocol + '://' + host + '/favicon.' + ext;
  288. } else {
  289. img.title = "Failed to find favicon for " + host;
  290. img.src = protocol + '://www.google.com/s2/favicons?domain=' + host; // Google's cache will sometimes provide a favicon we would have missed, e.g. if the site uses .png instead of .ico. Thanks to NV for suggesting this, and to Google.
  291. // @consider We could also generate an md5sum and request a gravatar, which might simply allow human recognition of repeats.
  292. img.removeEventListener('error',tryExtension,true);
  293. }
  294. }
  295. img.addEventListener('error',tryExtension,true);
  296. tryExtension();
  297.  
  298. img.title = ''+host;
  299. img.style.border = '0';
  300. img.style.paddingRight = '4px';
  301. img.style.width = '1.0em';
  302. img.style.height = '1.0em';
  303. // Favicon image elements can be hidden until they have fully loaded
  304. // img.style.display = 'none';
  305. img.addEventListener('load',function(){ img.style.display = ''; },false);
  306.  
  307. link.parentNode.insertBefore(img,link);
  308. }
  309.  
  310. if (!this.GM_addStyle) {
  311. this.GM_addStyle = function(css) {
  312. var s = document.createElement("style");
  313. s.type = 'text/css';
  314. s.innerHTML = css;
  315. document.getElementsByTagName("head")[0].appendChild(s);
  316. };
  317. }
  318.  
  319.  
  320.  
  321. // We consider related links, or "siblings", to be those on the current page
  322. // with the same DOM path as the clicked link.
  323.  
  324. // TODO: We should change the rules to track ancestors/descendants in a tree
  325. // for mailing lists archives like this MHonArc page:
  326. // http://www.redhat.com/archives/taroon-list/2007-August/thread.html
  327.  
  328. function getGroupSignature(link) {
  329. if (!link.cachedGroupSignature) {
  330. // We remove offsets like [4] from the unique xpath to get a more general path signature.
  331. link.cachedGroupSignature = getXPath(link).replace(/\[[0-9]*\]/g,'');
  332. }
  333. return link.cachedGroupSignature;
  334. }
  335.  
  336. function collectLinksInSameGroupAs(clickedLink) {
  337. // We remove the numbers from the XPath
  338. var seekXPath = getGroupSignature(clickedLink);
  339. // NOTE: We could search for matches with document.query - it might be faster.
  340. var links = document.getElementsByTagName("A");
  341. var collected = [];
  342. var lastLink = null;
  343. for (var i=0;i<links.length;i++) {
  344. var link = links[i];
  345. if (groupLinksByClass && link.className != clickedLink.className) {
  346. continue;
  347. }
  348. var xpath = getGroupSignature(link);
  349. if (xpath == seekXPath) {
  350. if (link.href !== lastLink) {
  351. // CONSIDER TODO: If no textContent, invent one? Otherwise this will never group links of e.g. thumbnails.
  352. if (link.textContent && link.textContent.trim()) { // ignore if no title
  353. collected.push(link);
  354. lastLink = link.href;
  355. }
  356. }
  357. }
  358. }
  359. if (verbose) {
  360. GM_log("Got "+collected.length+" matching siblings: for "+clickedLink.outerHTML+" with xpath "+seekXPath);
  361. }
  362. return collected;
  363. }
  364.  
  365. // Collect siblings when the user clicks a link, and pass them forward to the
  366. // target page in a hash package.
  367.  
  368. function isSuitable(link) {
  369.  
  370. if (link.tagName != "A") {
  371. return false;
  372. }
  373.  
  374. /*
  375. if (link.href.indexOf("#siblings=")>=0 || link.href.indexOf("&siblings=")>=0) {
  376. // This is already a prepared link! Probably created by the pager.
  377. // No need to modify it.
  378. return false;
  379. }
  380. */
  381. // The above check doesn't work if we are using passPacketByGM, so:
  382. if (link.isRLPPagerLink) {
  383. return false;
  384. }
  385. if (link.protocol.indexOf(/*"http") != 0)*/ "javascript:") == 0) {
  386. // We should not add #s to javascript: links but it seems to work ok on ftp:// (FF)
  387. return false;
  388. }
  389.  
  390. // Ignore links which are simply anchor into the current page
  391. // Note that .href gives the whole URL, so we check getAttribute("href")
  392. if (link.getAttribute("href") && link.getAttribute("href").charAt(0) == '#') {
  393. return false;
  394. }
  395.  
  396. // What about links to #s in other pages? I decided in the end to leave them
  397. // alone by default (preserve the existing hash string).
  398. // Altering # strings can break the way some sites use # strings, and can
  399. // prevent the browser from scrolling to the anchor.
  400. // You can force appending of siblings package to hash strings using '&' if
  401. // desired by disabling leaveHashUrlsAlone.
  402. // Perhaps in these "emergency" circumstances, we should append with '&' or
  403. // '?' *outside* the hash. (Which will no doubt break some sites, but
  404. // perhaps fewer!)
  405. if (link.hash && leaveHashUrlsAlone) {
  406. return false;
  407. }
  408.  
  409. if (beFrugal && !canPassPacketByGM(link,[]) && document.location.host.indexOf("google") == -1) {
  410. return false;
  411. }
  412.  
  413. //// === Some sites complain about long URLs or unexpected strings. ===
  414.  
  415. // Some Google search pages complain about our long URLs.
  416. var googleWillComplain = (
  417. link.host.indexOf("google")>=0 &&
  418. ( link.href.indexOf("?q=")>=0
  419. || link.href.indexOf("&q=")>=0
  420. || link.href.indexOf("url?")>=0
  421. )
  422. );
  423. // TODO: There are more of these cases on Google! (When earlier rewriting failed?)
  424.  
  425. var isYouTubePagerLink =
  426. link.host.indexOf("www.youtube.") == 0
  427. && link.href.indexOf("/all_comments?") >= 0;
  428. var youtubeWillComplain = isYouTubePagerLink;
  429.  
  430. var siteWillComplain = googleWillComplain || youtubeWillComplain;
  431.  
  432. if (siteWillComplain) {
  433. return false;
  434. }
  435.  
  436. // Even with forceTravel=false, we still manage to break github's use of history API.
  437. // Oh, that's not us. They have just changed their site this week! OK then let's keep things how they are for now.
  438. /*
  439. if (document.location.host == "github.com") {
  440. if (link.host == document.location.host) {
  441. //GM_log("Skipping github local link: dlh="+document.location.host+" lh="+link.host);
  442. return false;
  443. }
  444. }
  445. */
  446.  
  447. return true;
  448.  
  449. }
  450.  
  451. function canPassPacketByGM(link, siblings) {
  452. // Yes if we are in Firefox Greasemonkey
  453. if (typeof GM_setValue == 'function' && !window.navigator.vendor.match(/Google/)) {
  454. return true;
  455. }
  456. // Yes we can use our Fake shim above, if we are in Chrome, and travelling to the same host.
  457. if (link.host == document.location.host && useLocalStorageWhenPossible) {
  458. return true;
  459. }
  460. return false;
  461. }
  462.  
  463. function checkClick(evt) {
  464. // var elem = evt.target || evt.sourceElement;
  465. var elem = seekLinkInAncestry(evt.target || evt.sourceElement);
  466. // GM_log("Intercepted click event on "+getXPath(elem));
  467.  
  468. // Do not interfere with Ctrl-click or Shift-click or right-click (usually open-in-new-window/tab)
  469. if ((evt.ctrlKey && !enableOnCtrlClick) || (evt.shiftKey && !enableOnShiftClick) || (evt.button>0 && !enableOnRightClick)) {
  470. return;
  471. }
  472.  
  473. if (elem.tagName == "A") {
  474. var link = elem;
  475.  
  476. if (!isSuitable(link)) {
  477. return;
  478. }
  479.  
  480. // GM_log("User clicked on link: "+link.href);
  481. // Collect other links matching this one:
  482. var linksInGroup = collectLinksInSameGroupAs(link);
  483. // Convert from links to records:
  484. var siblings = linksInGroup.map(function(l) {
  485. var record = [l.textContent, l.href];
  486. if (l.href == link.href) {
  487. record[2] = 1; // Mark this record as the (soon-to-be) current one
  488. }
  489. return record;
  490. });
  491. if (siblings.length <= minimumGroupSize) {
  492. // No point. Give the user a clean location bar for a change. ;)
  493. return;
  494. }
  495. if (siblings.length > maximumGroupSize) {
  496. // It would be dangerous to proceed!
  497. return;
  498. }
  499.  
  500. siblings = JSON.stringify(siblings);
  501.  
  502. // I like to clear our highlights before travel, nice feedback to see something changed.
  503. if (highlightLinkGroups) {
  504. clearList();
  505. listOfHighlightedNodes = linksInGroup;
  506. highlightList(link, visitingColor);
  507. }
  508.  
  509. if (canPassPacketByGM(link,siblings)) {
  510. // GM_log("[RLP] Saving siblings_data");
  511. if (clearDataTimer) {
  512. clearTimeout(clearDataTimer);
  513. }
  514. GM_setValue("siblings_data",siblings);
  515. // GM_log("[RLP] Saving done");
  516. return; // Let the event occur naturally, if we do load a new page the packet will be picked up.
  517. }
  518.  
  519. // GM_log("Found "+siblings.length+" siblings for the clicked link.");
  520. var sibsEncoded = encodeURIComponent(siblings);
  521. // If the link already had a #, we append our data as an & parameter, and cross our fingers.
  522. var appendChar = ( link.href.indexOf('#')>=0 ? '&' : '#' );
  523. if (appendChar == '&' || link.hash) {
  524. if (verbose) {
  525. GM_log("Appending to existing hash with "+appendChar+": "+link.hash);
  526. }
  527. // Note: If it was a normal # to an anchor then we have probably broken
  528. // it! In that case we should either not append, or perhaps we can
  529. // append, but force movement to the correct anchor anyway (which may be
  530. // on the current page, or after navigation!).
  531. }
  532. var targetURL = link.href + appendChar + "siblings="+sibsEncoded;
  533.  
  534. // We need this on Google search result pages, or we end up following
  535. // feedback/tracking redirection links, which throw away our hash data!
  536. /*
  537. link.removeAttribute('onmousedown');
  538. // Thanks to http://userscripts.org/scripts/review/57679
  539. // Stopped working Oct 2012.
  540. */
  541. // Alternative fix see CHECK_IF_GOOGLE.
  542. /*
  543. if (onAGoogleSearchPage()) {
  544. forceTravel = true;
  545. }
  546. */
  547.  
  548. // Force travel to the new URL. (Don't wait for the page to handle the
  549. // click - some sites e.g. YouTube will throw away our hash-data!)
  550. if (forceTravel) {
  551. // We only do this for normal left-clicks.
  552. if (!evt.ctrlKey && !evt.shiftKey && evt.button==0) {
  553. document.location = targetURL;
  554. evt.preventDefault();
  555. evt.stopPropagation();
  556. return false;
  557. }
  558. }
  559.  
  560. if (verbose) {
  561. GM_log("Rewriting link "+getXPath(link));
  562. GM_log(" url: "+link.href);
  563. GM_log("with: "+targetURL);
  564. }
  565.  
  566. // Instead of pushing the browser to the magic URL, just change the link and see what happens.
  567. link.href = targetURL;
  568.  
  569. // CHECK_IF_GOOGLE
  570. // In the second half of 2012, Google's events got more powerful.
  571. // stopPropagation manages to work around this.
  572. // But we only do it on Google for now - we let other sites override us if they wanna (they might need to!).
  573. // This seemed to be working, but is not solving the problem any longer.
  574. // The problem appears to be that they are rewriting the href before we add #siblings!
  575. if (onAGoogleSearchPage()) {
  576. // evt.preventDefault();
  577. evt.stopPropagation();
  578. // return false;
  579. }
  580.  
  581. }
  582. }
  583.  
  584. document.body.addEventListener("click",checkClick,true);
  585. document.body.addEventListener("mousedown",checkClick,true);
  586. document.body.addEventListener("mouseup",checkClick,true);
  587.  
  588.  
  589.  
  590. // If we have been passed a hash package of siblings, present the lovely pager.
  591.  
  592. function createRelatedLinksPager(siblings) {
  593.  
  594. //// Find currentIndex.
  595. var hashPart = new RegExp("#.*");
  596. var seekURL = document.location.href.replace(hashPart,'');
  597. // var currentIndex = siblings.indexOf(seekURL); // No because the list contains records not urls!
  598. var currentIndex = -1;
  599. for (var i=0;i<siblings.length;i++) {
  600. var record = siblings[i];
  601. /*
  602. //// KNOWN BUG: This can fail if the receiving website redirects us, e.g. blogspot.com pushes me to the same page on blogspot.co.uk.
  603. //// Poor solution: Use wordex to find closest match.
  604. //// Good solution: TODO: Pass forward index along with siblings, just in case.
  605. if (record[1].replace(hashPart,'') == seekURL) {
  606. */
  607. if (record[2]) {
  608. currentIndex = i;
  609. break;
  610. }
  611. }
  612. // GM_log("Current index: "+currentIndex);
  613. if (currentIndex == -1) {
  614. // This should be unlikely to happen!
  615. GM_log("Odd, I could not find: "+seekURL+" in the siblings list.");
  616. // But it does happen occasionally.
  617. // One time by navigating to Wikipedia's main page by clicking the top-left logo.
  618. // Don't return. Show the list anyway!
  619. // return;
  620. }
  621.  
  622. if (showPageNumberInWindowTitle) {
  623. document.title = document.title + " (Page "+(currentIndex+1)+" of "+siblings.length+")";
  624. }
  625.  
  626. var pager = document.createElement("div");
  627. // Size of the pager is actually determined by its children. But we want to
  628. // remove any size constraints inherited from the page.
  629.  
  630. // Also in table_of_contents_everywhere.user.js
  631. // See also: clearStyle
  632. var resetProps = " width: auto; height: auto; max-width: none; max-height: none; ";
  633.  
  634. pager.id = "linkGroupPager";
  635. GM_addStyle("#linkGroupPager { "+resetProps+" position: fixed; top: 5%; right: 5%; "+
  636. "z-index: 9999999999; background: white; color: black; border: 1px solid black; "+
  637. "padding: 5px; font-size: 100%; text-align: center; } "+
  638. ".linkGroupPagerList { text-align: left; overflow: auto; }"
  639. );
  640.  
  641. function maybeHost(link) {
  642. if (link.host != document.location.host) {
  643. return "("+link.host+")";
  644. } else {
  645. return "";
  646. }
  647. }
  648.  
  649. function createLinkFromRecord(selectedRecord, text) {
  650. var link = document.createElement("A");
  651. link.textContent = text;
  652. var appendChar = ( selectedRecord[1].indexOf('#')>=0 ? '&' : '#' );
  653.  
  654. // Move the "current page marker" to the newly selected page
  655. var records = siblings;
  656. var newRecords = records.map(function(record) {
  657. record = record.slice(0); // clone to preserve original
  658. if (record[2]) {
  659. record.pop();
  660. }
  661. if (record[1] === selectedRecord[1]) {
  662. record[2] = 1;
  663. }
  664. return record;
  665. });
  666. var newSiblingsList = JSON.stringify(newRecords);
  667.  
  668. link.isRLPPagerLink = true;
  669. link.href = selectedRecord[1];
  670. if (canPassPacketByGM(link,siblings)) {
  671. // We wait and set the packet only when the user clicks, since GM_setValue is a single global. He may have gone browsing in another tab, using RLP there also and overwriting the packet, before coming back to click in this tab.
  672. link.addEventListener("click",function(e){
  673. if (clearDataTimer) {
  674. clearTimeout(clearDataTimer);
  675. }
  676. GM_setValue("siblings_data",newSiblingsList);
  677. },false);
  678. } else {
  679. link.href = selectedRecord[1] + appendChar + 'siblings=' + encodeURIComponent(newSiblingsList);
  680. }
  681. if (text != selectedRecord[0]) {
  682. link.title = selectedRecord[0];
  683. }
  684. link.title = (link.title ? link.title+' ' : '') + maybeHost(link);
  685. link.onclick = function(evt){
  686. if (!keepNavigationHistory) {
  687. if (evt.ctrlKey || evt.metaKey) {
  688. // User is trying to open this link in a new tab. Don't disturb her!
  689. } else {
  690. // Navigate sideways (not forwards). History will not remember current page.
  691. document.location.replace(this.href);
  692. evt.preventDefault();
  693. }
  694. }
  695. };
  696. return link;
  697. }
  698.  
  699. if (currentIndex > 0) {
  700. var leftRecord = siblings[currentIndex-1];
  701. var leftLink = createLinkFromRecord(leftRecord, "<<");
  702. leftLink.title = "Previous: "+leftLink.title;
  703. pager.appendChild(leftLink);
  704. }
  705.  
  706. // var pagerButton = document.createTextNode(" Pager ");
  707. var pagerButton = document.createElement("span");
  708. // pagerButton.textContent = " Pager ";
  709. pagerButton.textContent = " Page "+(currentIndex+1)+" of "+siblings.length+" ";
  710. pagerButton.addEventListener("click",function(evt) {
  711. pageList.style.display = ( pageList.style.display == 'none' ? '' : 'none' );
  712. },false);
  713. pagerButton.style.cursor = 'pointer';
  714. pager.appendChild(pagerButton);
  715.  
  716. if (currentIndex < siblings.length-1) {
  717. var rightRecord = siblings[currentIndex+1];
  718. var rightLink = createLinkFromRecord(rightRecord, ">>");
  719. rightLink.title = "Next: "+rightLink.title;
  720. pager.appendChild(rightLink);
  721. }
  722.  
  723. var closeButton = document.createElement("span");
  724. closeButton.textContent = "[X]";
  725. closeButton.style.cursor = 'pointer';
  726. closeButton.style.paddingLeft = '5px';
  727. closeButton.onclick = function() { pager.parentNode.removeChild(pager); };
  728. pager.appendChild(closeButton);
  729.  
  730. // We could create this lazily, but why not immediately? :P
  731. var pageList = document.createElement("div");
  732. pageList.className = "linkGroupPagerList";
  733. //// Un-DRY - these are also %ages in the GM_addStyle above.
  734. // pageList.style.maxWidth = (window.innerWidth * 0.40 | 0) + "px";
  735. // pageList.style.maxHeight = (window.innerHeight * 0.90 | 0) + "px";
  736. for (var i=0;i<siblings.length;i++) {
  737. pageList.appendChild(document.createElement("br"));
  738. pageList.appendChild(document.createTextNode(""+(i+1)+". "));
  739. var record = siblings[i];
  740. var text = record[0] || record[1]; // use address if no title
  741. var link = createLinkFromRecord(record, text);
  742. // if (record[1] == seekURL) {
  743. // if (record[1].replace(hashPart,'') == seekURL) {
  744. if (record[2]) {
  745. // Replace link with just a bold span
  746. var span = document.createElement("span");
  747. span.style.fontWeight = 'bold';
  748. span.textContent = link.textContent;
  749. link = span;
  750. }
  751. pageList.appendChild(link);
  752. addFaviconToLinkObviouslyIMeanWhyWouldntYou(link);
  753. }
  754. pageList.style.display = 'none';
  755. pager.appendChild(pageList);
  756.  
  757. // GM_log("Created pager: "+pager);
  758. document.body.appendChild(pager);
  759. }
  760.  
  761. if (grabbedList) {
  762. var siblings = JSON.parse(grabbedList);
  763. createRelatedLinksPager(siblings);
  764. if (clearDataFromLocation) {
  765. if (window.history.replaceState) {
  766. // Remove the siblings packet if it is found. Try to preserve anything else in the URL.
  767. var urlWithoutSiblingsPacket = document.location.href.replace(/(#siblings=.*|[?&]siblings=[^&]*)/,'');
  768. if (urlWithoutSiblingsPacket != document.location.href) {
  769. window.history.replaceState(null, null, urlWithoutSiblingsPacket);
  770. }
  771. } else {
  772. // Just cleanup by adjusting the #.....
  773. //document.location.hash = "."; // BAD. "#." breaks google search results pages, tho we rarely page through them.
  774. document.location.hash = ''; // Creates an extra history step, but the user may want that, to retain the data!
  775. //document.location.replace('#'); // Does not create history. Data lost! Fine if only navigating forwards.
  776. }
  777. }
  778. }
  779.  
  780.  
  781.  
  782. // Optional: Show link's siblings on hover
  783.  
  784. if (highlightLinkGroups) {
  785.  
  786. function seekLinkInAncestry(startElem) {
  787. var node = startElem;
  788. while (node) {
  789. if (node.tagName == "A") {
  790. return node;
  791. }
  792. node = node.parentNode;
  793. }
  794. return startElem;
  795. }
  796.  
  797. var listOfHighlightedNodes = [];
  798.  
  799. var directions = ["Top","Bottom","Left","Right"];
  800.  
  801. function highlightList(link, highlightColor, thisLinkHighlightColor) {
  802. if (verbose) {
  803. GM_log("Highlighting "+listOfHighlightedNodes.length+" elements.");
  804. }
  805. for (var i=0;i<listOfHighlightedNodes.length;i++) {
  806. var elem = listOfHighlightedNodes[i];
  807. var style = getComputedStyle(elem,null);
  808. if (highlightColor) {
  809. if (changeBackgroundNotBorder) {
  810. elem.savedOldBackgroundColor = elem.style.backgroundColor;
  811. if (thisLinkHighlightColor && elem == link) {
  812. link.style.backgroundColor = thisLinkHighlightColor;
  813. } else {
  814. elem.style.backgroundColor = highlightColor;
  815. }
  816. } else {
  817. for (var dir in directions) {
  818. dir = directions[dir];
  819. elem["savedOldBorder"+dir] = style["border"+dir];
  820. elem["savedOldMargin"+dir] = style["margin"+dir];
  821. elem["savedOldPadding"+dir] = style["padding"+dir];
  822. // parseInt will drop any "px", but produces NaN on "", so we |0 that.
  823. if (thisLinkHighlightColor && elem == link) {
  824. link.style["border"+dir] = ((parseInt(style["border"+dir])|0)+1)+"px "+lineStyle+" "+thisLinkHighlightColor;
  825. } else {
  826. elem.style["border"+dir] = ((parseInt(style["border"+dir])|0)+1)+"px "+lineStyle+" "+highlightColor;
  827. }
  828. // Since we added 1px to the border, we subtract 1px from the margin.
  829. elem.style["margin"+dir] = ((parseInt(style["margin"+dir])|0)-1)+"px";
  830. // elem.style["padding"+dir] = ((parseInt(style["padding"+dir])|0)+1)+"px";
  831. }
  832. }
  833. }
  834. }
  835. }
  836.  
  837. function clearList(retainList) {
  838. for (var i=0;i<listOfHighlightedNodes.length;i++) {
  839. var elem = listOfHighlightedNodes[i];
  840. if (changeBackgroundNotBorder) {
  841. elem.style.backgroundColor = elem.savedOldBackgroundColor;
  842. } else {
  843. for (var dir in directions) {
  844. dir = directions[dir];
  845. elem.style["border"+dir] = elem["savedOldBorder"+dir];
  846. elem.style["margin"+dir] = elem["savedOldMargin"+dir];
  847. elem.style["padding"+dir] = elem["savedOldPadding"+dir];
  848. }
  849. }
  850. }
  851. listOfHighlightedNodes.length = 0;
  852. }
  853.  
  854. document.body.addEventListener("mouseover",function(evt) {
  855. var link = seekLinkInAncestry(evt.target || evt.sourceElement);
  856. if (isSuitable(link)) {
  857. clearList();
  858. listOfHighlightedNodes = collectLinksInSameGroupAs(link);
  859. if (showGroupCountInLinkTitle && !link.doneAppendGroupsize) {
  860. link.doneAppendGroupsize = true;
  861. link.title = (link.title ? link.title+" " : "") + "("+listOfHighlightedNodes.length+" related links)";
  862. }
  863. if (listOfHighlightedNodes.length>=minimumGroupSize && listOfHighlightedNodes.length<maximumGroupSize) {
  864. highlightList(link, highlightColor, thisLinkHighlightColor);
  865. }
  866. }
  867. },true);
  868. document.body.addEventListener("mouseout",function(evt) {
  869. var link = seekLinkInAncestry(evt.target || evt.sourceElement);
  870. if (isSuitable(link)) {
  871. clearList();
  872. }
  873. },true);
  874.  
  875. }
  876.  
  877.  
  878.  
  879. }
  880.  
  881.  
  882.  
  883. setTimeout(runRelatedLinksPager, delayBeforeRunning);
  884.  
  885.  
  886.  
  887.  
  888. if ( document.location.href.match("last.fm/.*page=") ) {
  889. GM_addStyle("a:visited { color: darkblue; }")
  890. }