MetaFilter Filter By Favorites

Allows users to view MetaFilter comments by favorite count.

当前为 2014-10-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MetaFilter Filter By Favorites
  3. // @namespace http://namespace.kinobe.com/metafilter/
  4. // @description Allows users to view MetaFilter comments by favorite count.
  5. // @include /^https?://(www|ask|metatalk|fanfare|projects|music|irl)\.metafilter\.com/.*$/
  6. // @include http://mefi/*
  7. // @version 1.0
  8. // @grant GM_addStyle
  9. // ==/UserScript==
  10.  
  11. /*
  12.  
  13. This copyright section and all credits in the script must be included in modifications or redistributions of this script.
  14.  
  15. MetaFilterFilterByFavorites is Copyright (c) 2014, Jonathan Gordon
  16. MetaFilterFilterByFavorites is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License
  17. License information is available here: http://creativecommons.org/licenses/by-sa/3.0/
  18.  
  19. */
  20.  
  21. /*
  22. This script borrows heavily from Jimmy Woods' MetaFilter favorite posts filter script
  23. http://userscripts.org/scripts/show/75332
  24.  
  25. Also from Jordan Reiter's MetaFilter MultiFavorited Multiwidth - November Experiment
  26. http://userscripts.org/scripts/show/61012
  27.  
  28. Please see the README.md for more info:
  29.  
  30. https://greasyfork.org/scripts/5717-metafilter-filter-by-favorites
  31. Version 1.0
  32. - Initial Release.
  33. */
  34.  
  35. var LogLevelEnum = {
  36. DEBUG:{value:0, name:"Debug"},
  37. INFO:{value:1, name:"Info"},
  38. WARN:{value:2, name:"Warn"},
  39. ERROR:{value:3, name:"Error"}
  40. };
  41.  
  42. var SiteEnum = {
  43. WWW:{
  44. name:"www", titleRE:/^.+?\| MetaFilter$/, fav_prefix:"2"
  45. }, ASK:{
  46. name:"ask", titleRE:/^.+?\| Ask MetaFilter$/, fav_prefix:"4"
  47. }, TALK:{
  48. name:"talk", titleRE:/^.+?\| MetaTalk$/, fav_prefix:"6"
  49. }, PROJECTS:{
  50. name:"projects", titleRE:/^.+?\| MetaFilter Projects$/, fav_prefix:"13"
  51. }, MUSIC:{
  52. name:"music", titleRE:/^.+?\| MeFi Music$/, fav_prefix:"9"
  53. }, IRL:{
  54. name:"irl", titleRE:/^.+?\| IRL: MeFi Events$/, fav_prefix:"20"
  55. }, FANFARE:{
  56. name:"fanfare", titleRE:/^.+?\| FanFare$/, fav_prefix:"24"
  57. }
  58. };
  59.  
  60. Global = {
  61. last_tr:null // Reference to the last TR tag in the select table that a user clicked on.
  62. , table_bg_color:"#E6E6E6" // Background color for the table rows.
  63. , selected_color:"#88c2d8" // BG color for the selected table row.
  64. , hover_color:"#DC5E04" // BG color for the selected table row.
  65. , favorite_color:"#ff7617" // BG color for the selected table row.
  66. , max_count:100 // Largest possible # of favorites
  67. , min_count:0 // Smallest # of favorites that are highlighted
  68. , posts:[] // Stores info about each post
  69. , max_favorites:0 // Highest favorite count so far.
  70. , doLog:true // Should we log messages?
  71. , row_prefix:"summary_id_" // Used to set the ID for each row in the comment/favorite chart
  72. , logLevel:LogLevelEnum.INFO // What's the default log level?
  73. };
  74.  
  75.  
  76. /**
  77. * ----------------------------------
  78. * Logger
  79. * ----------------------------------
  80. * Allows swapping out GM logger for console
  81. */
  82. Logger = {
  83.  
  84. log:function (message, logLevelEnum) {
  85. logLevelEnum = logLevelEnum || LogLevelEnum.INFO;
  86.  
  87. if (Global.doLog && logLevelEnum.value >= Global.logLevel.value) {
  88. // GM_log(message);
  89. console.log(message);
  90. }
  91. }, debug:function (message) {
  92. Logger.log(message, LogLevelEnum.DEBUG);
  93. }, info:function (message) {
  94. Logger.log(message, LogLevelEnum.INFO);
  95. }, warn:function (message) {
  96. Logger.log(message, LogLevelEnum.WARN);
  97. }, error:function (message) {
  98. Logger.log(message, LogLevelEnum.ERROR);
  99. }
  100. };
  101.  
  102. /**
  103. * ----------------------------------
  104. * Util
  105. * ----------------------------------
  106. * Various utility functions
  107. */
  108. Util = {
  109. /**
  110. * Returns an array of DOM elements that match a given XPath expression.
  111. *
  112. * @param path string - Xpath expression to search for
  113. * @param from DOM Element - DOM element to search under. If not specified, document is used
  114. * @return Array - Array of selected nodes (if any)
  115. */
  116. getNodes:function (path, from) {
  117.  
  118. Logger.debug("getNodes of path: " + path);
  119.  
  120. from = from || document;
  121.  
  122. var item, ret = [];
  123. var iterator = document.evaluate(path, from, null, XPathResult.ANY_TYPE, null);
  124. while (item = iterator.iterateNext()) {
  125. ret.push(item);
  126. // Logger.debug("Item is: "+item);
  127.  
  128. }
  129. Logger.debug("Num elements found by getNodes: " + ret.length);
  130. return ret;
  131. }
  132.  
  133. /**
  134. * Deletes a DOM element
  135. * @param DOM element - DOM element to remove
  136. * @return DOM element - the removed element
  137. */, removeElement:function (element) {
  138. return element.parentNode.removeChild(element);
  139. }
  140.  
  141. /**
  142. * Binds an event handler function to an object context, so that the handler can be executed as if it
  143. * was called using "this.<method name>(event)", i.e. it can use "this.foo" inside it.
  144. *
  145. * @param function method - a function to execute as an event handler
  146. * @param Object context - the object that will be used as context for the function, as if the function had been
  147. * called as context.method(event);
  148. * @return function - the function to pass to addEventListener
  149. */, bindAsEventHandler:function (method, context) {
  150. var __method = method;
  151. return function (event) {
  152. return __method.apply(context, [event]);
  153. }
  154. }
  155. };
  156.  
  157. /*
  158. * Event handler for when user clicks on a row
  159. */
  160. function filterPosts(evt) {
  161. // Find the parent <TR> tag.
  162.  
  163. Logger.debug("filterPosts");
  164. var t = evt.target;
  165. Logger.debug("t: " + t);
  166. while (null == t.getAttribute("id")) {
  167. Logger.debug("Looking for DIV");
  168. t = t.parentNode;
  169. }
  170.  
  171. var summary_id = t.getAttribute('id');
  172. Logger.debug("t.id: " + summary_id);
  173. var summary_row_re = /^summary_id_(\d+)$/;
  174. var max_cnt = (summary_row_re.exec(summary_id) !== null) ? parseInt(RegExp.$1) : 0;
  175.  
  176. Logger.debug("Parsed max_cnt: " + max_cnt);
  177.  
  178.  
  179. // Hide/unhide all posts that don't match the chosen fav count.
  180. var i = Global.posts.length;
  181. while (i--) {
  182. var is_showing = (Global.posts[i].div.style.display !== "none");
  183. var do_show = (Global.posts[i].num_favs >= max_cnt);
  184.  
  185. Logger.debug("is_showing: " + is_showing);
  186. Logger.debug("do_show: " + do_show);
  187.  
  188. if (do_show != is_showing) {
  189. Logger.debug("Hiding post: " + i);
  190.  
  191. Global.posts[i].div.style.display = (do_show ? "" : "none");
  192. Global.posts[i].div.nextSibling.style.display = (do_show ? "" : "none");
  193. Global.posts[i].div.nextSibling.nextSibling.style.display = (do_show ? "" : "none");
  194. }
  195. }
  196.  
  197. // Reset the color of the previous row to be clicked on.
  198. if (Global.last_tr !== null) {
  199. Logger.debug("Resetting the background color.");
  200.  
  201. removeClass(Global.last_tr, "wrapperSelected");
  202.  
  203. }
  204. // Set the color of the row we just clicked on
  205. addClass(t, "wrapperSelected");
  206. Global.last_tr = t;
  207. }
  208.  
  209. function addClass(obj, className) {
  210. if (null != obj && undefined != obj) {
  211. var prevClass = obj.className;
  212.  
  213. if (null != prevClass && undefined != prevClass) {
  214. if (!prevClass.match(new RegExp(className))) {
  215. obj.className = obj.className + " " + className;
  216. }
  217. }
  218. }
  219. }
  220.  
  221. function removeClass(obj, className) {
  222. if (null != obj && undefined != obj) {
  223. var prevClass = obj.className;
  224.  
  225. if (null != prevClass && undefined != prevClass) {
  226. var regExp = new RegExp(className);
  227. if (prevClass.match(regExp)) {
  228. obj.className = obj.className.replace(regExp, '');
  229. }
  230. }
  231. }
  232.  
  233. }
  234.  
  235. // ---------------------------
  236.  
  237. //Finds y value of given object
  238. function findPos(obj) {
  239. var current_top = 0;
  240. if (obj.offsetParent) {
  241. do {
  242. current_top += obj.offsetTop;
  243. } while (obj = obj.offsetParent);
  244. }
  245. return current_top;
  246. }
  247. function simulateClickShow(id) {
  248.  
  249. // jquery isn't working here
  250. // $('#filter0').trigger('click');
  251.  
  252. // use non-jquery method to simulate the click of the count row specified
  253. var evt = document.createEvent("MouseEvents");
  254. evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  255. var show_all_link = document.getElementById(id);
  256. show_all_link.dispatchEvent(evt);
  257.  
  258. }
  259.  
  260. function getElementsByClassName(node, classname) {
  261. if (node.getElementsByClassName) { // use native implementation if available
  262. Logger.debug("Using native implementation of getElementsByClassName.");
  263. return node.getElementsByClassName(classname);
  264. } else {
  265. return (function getElementsByClass(searchClass, node) {
  266. node = node || document;
  267. var classElements = [], els = document.getElementsByTagName("*"), elsLen = els.length, pattern = new RegExp("(^|\\s)" + searchClass + "(\\s|$)"), i, j;
  268. Logger.debug("Total elements: " + els.length);
  269. Logger.debug("Looking for" + searchClass);
  270.  
  271. for (i = 0, j = 0; i < elsLen; i++) {
  272.  
  273. var elsClassName = els[i].className;
  274. if ("" != elsClassName) {
  275. // Logger.debug("Class of element: " + elsClassName);
  276. }
  277. if (pattern.test(elsClassName)) {
  278. classElements[j] = els[i];
  279. j++;
  280. }
  281. }
  282. return classElements;
  283. })(classname, node);
  284. }
  285. }
  286.  
  287.  
  288. // a function that loads jQuery and calls a callback function when jQuery has finished loading
  289. function addJQuery(callback) {
  290. var script = document.createElement("script");
  291. script.setAttribute("src", "http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js");
  292. script.addEventListener('load', function () {
  293. var script = document.createElement("script");
  294. script.textContent = "(" + callback.toString() + ")();";
  295. document.body.appendChild(script);
  296. }, false);
  297. document.body.appendChild(script);
  298. }
  299.  
  300. function captureShowClick(e) {
  301.  
  302. var click_target = e.target;
  303. while (click_target.tagName != "SPAN") {
  304. click_target = click_target.parentNode;
  305. }
  306.  
  307. Logger.debug("e.target is: " + click_target);
  308. Logger.debug("e.target.id is: " + click_target.id);
  309.  
  310. var recommended_re = /^(\d+)_(\d+)$/;
  311.  
  312. var id = recommended_re.exec(click_target.id)[1];
  313. Logger.debug("ID is: " + id);
  314. var count = recommended_re.exec(click_target.id)[2];
  315. Logger.debug("Count is: " + count);
  316.  
  317. var comment_anchor = Util.getNodes('.//a[@name="' + id + '"]')[0];
  318. var prevPos = findPos(comment_anchor);
  319. Logger.debug("prevPos: " + prevPos);
  320. Logger.debug("Previous window.pageYOffset: " + window.pageYOffset);
  321.  
  322. var diff = prevPos - window.pageYOffset;
  323.  
  324. simulateClickShow(Global.row_prefix + count);
  325.  
  326. //Get object
  327. Logger.debug("Did we find SupportDiv? " + comment_anchor);
  328.  
  329. //Scroll to location of SupportDiv on load
  330. var newPos = findPos(comment_anchor);
  331. Logger.debug("newPos: " + newPos);
  332. Logger.debug("Current window.pageYOffset (before scrolling): " + window.pageYOffset);
  333.  
  334. window.scroll(0, newPos - diff);
  335. Logger.debug("Current window.pageYOffset (after scrolling): " + window.pageYOffset);
  336.  
  337. // simulateClickShow(id);
  338. return false;
  339. }
  340.  
  341. function getSite() {
  342.  
  343. // Which subsite are we on?
  344. var title = document.title;
  345. Logger.debug("document.title: >" + title + "<");
  346.  
  347. for (var propertyName in SiteEnum) {
  348. // propertyName is what you want
  349. if (SiteEnum[propertyName].titleRE.test(title)) {
  350. return SiteEnum[propertyName];
  351. }
  352. }
  353. return null;
  354.  
  355. }
  356.  
  357. //check if the previous sibling node is an element node
  358. function getPreviousElement(n) {
  359. var x = n.previousSibling;
  360. while (null != x && x.nodeType != 1) {
  361. x = x.previousSibling;
  362. Logger.debug("Previous sibling?: " + typeof x);
  363. Logger.debug("Previous sibling: " + x);
  364. }
  365. return x;
  366. }
  367. function init() {
  368. Logger.info("Loading MetaFilterFilterByFavorites...");
  369.  
  370. // if we can't find comments, it's probably this is being called for a page we haven't excluded
  371. if (undefined == document.getElementById("posts")) {
  372. Logger.info("MetaFilterFilterByFavorites can not find top node. Exiting.");
  373. return;
  374. }
  375.  
  376. Logger.debug("MetaFilterFilterByFavorites found top node. Continuing...");
  377.  
  378. var site = getSite();
  379.  
  380. if (null == site) {
  381. Logger.error("MetaFilterFilterByFavorites can not determine site. Exiting...");
  382. return;
  383. }
  384.  
  385. Logger.debug("site: " + site.name);
  386.  
  387. // Prepare array for storing counts of how many posts have been favorited this many times.
  388. var counts = [];
  389. for (var j = 0; j <= Global.max_count; j++) {
  390. counts[j] = 0;
  391. }
  392.  
  393. // some useful regexes for parsing ids and such
  394. var numeric_re = /^(\d+)$/, favorites_re = /^(\d+)\sfavorite[s]?$/;
  395.  
  396. // Get all comments and compile them into arrays
  397. var commentDivs = Util.getNodes('.//div[@id="posts"]//div[contains(concat(" ", normalize-space(@class), " "), " comments ")]');
  398.  
  399. Logger.debug("Num comments found: " + commentDivs.length);
  400.  
  401. // if there are no comments, don't show table
  402. if (0 == commentDivs.length) {
  403. Logger.info("MetaFilterFilterByFavorites can not find comments. Exiting.");
  404. return;
  405. }
  406.  
  407. for (var i = 0; i < commentDivs.length; i++) {
  408. Logger.debug("MetaFilterFilterByFavorites found comment div. Continuing...");
  409.  
  410. var comment_div = commentDivs[i];
  411. Logger.debug("Found comment_div: " + comment_div.textContent);
  412.  
  413. var sibling_a = getPreviousElement(comment_div);
  414.  
  415. // if the comment doesn't have a previous sibling, we're not interested
  416. if (null == sibling_a) {
  417. continue;
  418. }
  419.  
  420. Logger.debug("sibling_a: " + typeof sibling_a);
  421. Logger.debug("sibling_a.name: " + sibling_a.name);
  422.  
  423. var comment_div_id = sibling_a.name;
  424. // Logger.debug("Id is: " + comment_div_id);
  425. Logger.debug("comment_div_id: " + comment_div_id);
  426.  
  427. if (comment_div_id !== undefined && numeric_re.test(comment_div_id)) {
  428. Logger.debug("Found a valid id: " + comment_div_id);
  429.  
  430.  
  431. var fav_count_a = Util.getNodes('.//span[@id="favcnt' + site.fav_prefix + comment_div_id + '"]/a')[0];
  432. Logger.debug("fav_count_a: " + fav_count_a);
  433. Logger.debug("typeof fav_count_a: " + typeof fav_count_a);
  434.  
  435. var recommended_text = undefined !== fav_count_a ? fav_count_a.textContent : "0 favorites";
  436.  
  437. var favorite_count = (favorites_re.exec(recommended_text) !== null) ? Math.min(parseInt(RegExp.$1), Global.max_count) : 0;
  438. Logger.debug("favorite_count: " + favorite_count);
  439. counts[favorite_count]++;
  440. Logger.debug("Done pushing recommended_count: " + favorite_count);
  441.  
  442. // we only highlight if there's a fav count over the minimum
  443. if (favorite_count > Global.min_count) {
  444. Logger.debug("recommended_count > " + Global.min_count + ": " + favorite_count);
  445.  
  446. var recommendedWidthSize = (Math.round(favorite_count / 2) + 1);
  447. comment_div.style.borderLeft = '' + recommendedWidthSize + 'px solid ' + Global.favorite_color;
  448. comment_div.style.borderTop = '0px';
  449. comment_div.style.borderBottom = '0px';
  450. comment_div.style.paddingLeft = '5px';
  451. }
  452.  
  453.  
  454. Global.max_favorites = Math.max(favorite_count, Global.max_favorites);
  455.  
  456. Logger.debug("Calculating max_favorites:" + Global.max_favorites);
  457.  
  458. Global.posts.push({
  459. div:comment_div, num_favs:favorite_count
  460. });
  461. Logger.debug("Calculated max_favorites:" + favorite_count);
  462.  
  463. var id_text = comment_div_id + "_" + favorite_count;
  464. Logger.debug("id_text" + id_text);
  465. var all_id_text = comment_div_id + "_0";
  466. var show_all_span = document.createElement('span');
  467. show_all_span.className = "click_count";
  468. show_all_span.id = all_id_text;
  469.  
  470. var show_count_span = document.createElement('span');
  471. show_count_span.className = "click_count";
  472. show_count_span.id = id_text;
  473. show_all_span.innerHTML = "&nbsp;<a>Show: all</a>";
  474. show_count_span.innerHTML = "&nbsp;<a> / " + favorite_count + " and above</a>";
  475.  
  476. var show_more_span = document.createElement('span');
  477. show_more_span.innerHTML = "&nbsp;<a href='#posts'> / More options</a>";
  478.  
  479. var flag_div = Util.getNodes('.//span[@id="flag' + site.fav_prefix + comment_div_id + '"]', comment_div)[0];
  480. Logger.debug("Inserting show all");
  481. flag_div.parentNode.insertBefore(show_all_span, flag_div);
  482.  
  483. if (favorite_count > Global.min_count) {
  484. Logger.debug("Inserting show count");
  485. flag_div.parentNode.insertBefore(show_count_span, flag_div);
  486. }
  487.  
  488. Logger.debug("Inserting show more options");
  489. flag_div.parentNode.insertBefore(show_more_span, flag_div);
  490.  
  491. }
  492. }
  493. Logger.debug("Done looping through comments!");
  494.  
  495. GM_addStyle('#posts { margin-bottom: 1em; }');
  496.  
  497. GM_addStyle('.chart {'
  498. + 'background-color: ' + Global.table_bg_color + ';'
  499. + 'font: 14px sans-serif;'
  500. + 'margin: 0px 4px;'
  501. + 'color: black;'
  502. + 'border:1px solid white;'
  503. + 'border-collapse:collapse;'
  504. + '}');
  505.  
  506.  
  507. GM_addStyle('.comms {'
  508. + 'margin-left: 1em;'
  509. + 'float: left;'
  510. + 'width: 5%;'
  511. + '}');
  512.  
  513. GM_addStyle('.favs {'
  514. + 'float: left;'
  515. + 'background-color: ' + Global.favorite_color + ';'
  516. + 'margin-right: 4px;'
  517. + 'text-align: center;'
  518. + '}');
  519.  
  520. GM_addStyle('.wrapper {'
  521. + 'display: block;'
  522. + 'padding: 3px 0px;'
  523. + '}');
  524.  
  525. GM_addStyle('.wrapperSelected {'
  526. + 'background-color: ' + Global.selected_color + ';'
  527. + '}');
  528.  
  529. GM_addStyle('.wrapper:hover {'
  530. + 'background-color: ' + Global.hover_color + ';'
  531. + '}');
  532.  
  533. GM_addStyle('.clearfix:after {'
  534. + 'content: ".";'
  535. + 'display: block;'
  536. + 'height: 0;'
  537. + 'clear: both;'
  538. + 'visibility: hidden;'
  539. + '}');
  540.  
  541. Logger.debug("Done adding style.");
  542.  
  543. initTable(counts);
  544. document.addEventListener('keydown', function (e) {
  545. // pressed alt+g
  546. if (e.keyCode == 71 && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
  547. simulateClickShow(Global.row_prefix + 0);
  548. }
  549. }, false);
  550.  
  551. var allClickClasses = getElementsByClassName(document, "click_count");
  552. Logger.debug("allClickClasses count: " + allClickClasses.length);
  553.  
  554. for (var k = 0; k < allClickClasses.length; k++) {
  555. var n = allClickClasses[k];
  556. Logger.debug("n is: " + n);
  557. Logger.debug("n.target is: " + n.target);
  558. n.addEventListener('click', captureShowClick, false);
  559. }
  560.  
  561. Logger.info("Loading MetaFilterFilterByFavorites is complete.");
  562. }
  563.  
  564. /**
  565. * Generates the table at the top of the page
  566. * @param counts - Array of post counts, from 0 to Global.max_total. [fav_count => # of posts]
  567. * @return void
  568. */
  569. function initTable(counts) {
  570. Logger.debug("Total counts: " + counts);
  571.  
  572. var dummyDiv = document.createElement('div');
  573. var data_rows_html = '<div class="chart" style="width: 70%;">';
  574. var m = Global.max_count + 1, cum_comment_total = 0;
  575. // Generate the table rows
  576. while (m-- >= 0) {
  577.  
  578. // we only show differences where the comment count has increased, or the very last row, showing all
  579. if (counts[m] > 0 || m == 0) {
  580. cum_comment_total += counts[m];
  581.  
  582. var recommendedWidthSize = (Math.round((m / Global.max_favorites) * 90));
  583.  
  584. data_rows_html += '<div id="' + Global.row_prefix + m + '" class="wrapper clearfix"><div class="comms">' + cum_comment_total + '</div>'
  585. + '<div class="favs" style="width: ' + recommendedWidthSize + '%;">(' + ((m == 0) ? "All" : m) + ')</div>'
  586. + '</div>';
  587.  
  588. }
  589. }
  590.  
  591. // Insert table into page
  592.  
  593. Logger.debug("data_rows_html: " + data_rows_html);
  594. dummyDiv.innerHTML = '<div>'
  595. + '<div id="MultiFavoritesOptions" class="clearfix" style="white-space:nowrap; padding: 3px 0;">Show me this many comments (with at least this many favorites)</div>'
  596. + data_rows_html
  597. + '</div>';
  598. var page_div = document.getElementById("posts");
  599. page_div.insertBefore(dummyDiv.firstChild, page_div.firstChild);
  600.  
  601. // Add the event listeners.
  602. var rows = Util.getNodes('.//div[@class="wrapper clearfix"]');
  603. var n = rows.length;
  604. Logger.debug("Found rows: " + n);
  605.  
  606. while (n--) {
  607. Logger.debug("addEventListener");
  608. rows[n].addEventListener('click', filterPosts, false);
  609. }
  610. }
  611.  
  612. init();