FetLife Text Search

Searches through FetLife group discussions for a specific keyword or phrase.

  1. /**
  2. *
  3. * This is a Greasemonkey script and must be run using a Greasemonkey-compatible browser.
  4. *
  5. * @author maymay <bitetheappleback@gmail.com>
  6. */
  7. // ==UserScript==
  8. // @name FetLife Text Search
  9. // @version 0.1.1
  10. // @namespace com.maybemaimed.fetlife.textsearch
  11. // @description Searches through FetLife group discussions for a specific keyword or phrase.
  12. // @include https://fetlife.com/groups*
  13. // @exclude https://fetlife.com/adgear/*
  14. // @exclude https://fetlife.com/chat/*
  15. // @exclude https://fetlife.com/im_sessions*
  16. // @exclude https://fetlife.com/polling/*
  17. // @grant GM_xmlhttpRequest
  18. // @grant GM_addStyle
  19. // @grant GM_log
  20. // ==/UserScript==
  21.  
  22. FL_TXT = {};
  23. FL_TXT.CONFIG = {
  24. 'debug': false, // switch to true to debug.
  25. };
  26.  
  27. // Utility debugging function.
  28. FL_TXT.log = function (msg) {
  29. if (!FL_TXT.CONFIG.debug) { return; }
  30. GM_log('FETLIFE TEXT SEARCH: ' + msg);
  31. };
  32.  
  33. // Initializations.
  34. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
  35. GM_addStyle('\
  36. #tabnav li:last-child { position: relative; }\
  37. #tabnav li:last-child div {\
  38. opacity: .95;\
  39. display: none;\
  40. position: absolute;\
  41. background: gray;\
  42. width: 930px;\
  43. min-height: 125px;\
  44. padding: 3px 10px;\
  45. }\
  46. #tabnav li:last-child input[type="text"] { width: 100%; }\
  47. #tabnav #fetlife_text_search_results li {\
  48. opacity: 1;\
  49. display: list-item;\
  50. margin: 1em 0;\
  51. clear: left;\
  52. }\
  53. #tabnav #fetlife_text_search_results .avatar { float: left; }\
  54. #tabnav #fetlife_text_search_results .permalink {\
  55. font-weight: normal;\
  56. background-color: transparent;\
  57. padding: 0;\
  58. border: none;\
  59. color: black;\
  60. line-height: 1.3em;\
  61. }\
  62. .comment::target { border: 1px solid red; }\
  63. ');
  64. FL_TXT.init = function () {
  65. FL_TXT.currentSearch = {};
  66. FL_TXT.CONFIG.search_form = document.getElementById('tabnav');
  67. FL_TXT.main();
  68. };
  69. window.addEventListener('DOMContentLoaded', FL_TXT.init);
  70.  
  71. FL_TXT.getPageFromURL = function (url) {
  72. FL_TXT.log('Getting page from URL: ' + url);
  73. GM_xmlhttpRequest({
  74. 'method': 'GET',
  75. 'url': url,
  76. 'onload': function (response) {
  77.  
  78. // Parse page now to find pagination, in a moment.
  79. var parser = new DOMParser();
  80. var doc = parser.parseFromString(response.responseText, 'text/html');
  81.  
  82. // Set up next request.
  83. my_page = (url.match(/\?page=\d+$/)) ? parseInt(url.match(/\d+$/)[0]) : 1 ;
  84. next_page = my_page + 1;
  85. if (next_page > 2) {
  86. next_url = url.replace(/\d+$/, next_page.toString());
  87. } else {
  88. next_url = url + '?page=' + next_page.toString();
  89. }
  90.  
  91. // If this was a discussion listing page
  92. if (!url.match(/group_posts/)) {
  93. // Make a list of links to each thread, and recurse on those.
  94. var as = doc.querySelectorAll('#discussions h4 a');
  95. for (var i = 0; i < as.length; i++) {
  96. FL_TXT.getPageFromURL(as[i].href);
  97. }
  98.  
  99. // Check for pagination on discussion lists, follow if necessary.
  100. if (FL_TXT.isPaginated(doc)) {
  101. FL_TXT.getPageFromURL(next_url);
  102. }
  103. return;
  104. }
  105.  
  106. // Search group post content, too, if on first page.
  107. if (my_page === 1 && (-1 !== doc.querySelector('.group_post').textContent.toLowerCase().search(FL_TXT.currentSearch.search_string.toLowerCase()))) {
  108. FL_TXT.displayResult(doc.querySelector('.group_post'), doc.querySelector('.group_post').textContent.toLowerCase().search(FL_TXT.currentSearch.search_string.toLowerCase()), url);
  109. }
  110.  
  111. var els = doc.querySelectorAll('.comment');
  112.  
  113. result_count = 0;
  114. for (var i = 0; i < els.length; i++) {
  115. // Parse results for this page and make note of any search string hits.
  116. var x = els[i].textContent.toLowerCase().search(FL_TXT.currentSearch.search_string.toLowerCase());
  117. if (-1 !== x) {
  118. FL_TXT.displayResult(els[i], x, url);
  119. }
  120. result_count++;
  121. }
  122.  
  123. // Check for pagination of group thread itself, follow.
  124. if (FL_TXT.isPaginated(doc)) {
  125. FL_TXT.getPageFromURL(next_url);
  126. }
  127. }
  128. });
  129. };
  130.  
  131. FL_TXT.isPaginated = function (doc) {
  132. if (!doc.querySelector('.pagination')) {
  133. return false;
  134. } else if (!doc.querySelector('.pagination .next_page.disabled')) {
  135. return true;
  136. } else {
  137. return false;
  138. }
  139. };
  140.  
  141. FL_TXT.toggleSearchInterface = function (e) {
  142. var lis = e.target.parentNode.parentNode.children;
  143. for (var i = 0; i < lis.length; i++) {
  144. lis[i].className = lis[i].className.replace('in_section', '');
  145. }
  146. e.target.parentNode.className = "in_section";
  147. e.target.nextSibling.style.display = 'block';
  148. };
  149.  
  150. FL_TXT.getGroupName = function () {
  151. return document.querySelector('.group_name a').innerHTML;
  152. };
  153.  
  154. FL_TXT.doTextSearch = function (e) {
  155. e.preventDefault();
  156. var group_id = document.getElementById('fetlife_text_search_group_id').value;
  157. var search_string = document.getElementById('fetlife_text_search').value;
  158. FL_TXT.currentSearch.search_string = search_string;
  159. switch (e.explicitOriginalTarget.value) {
  160. case 'Search all discussions.':
  161. FL_TXT.searchGroup(search_string, group_id);
  162. break;
  163. case 'Search this thread.':
  164. default:
  165. var group_post_id = document.getElementById('fetlife_text_search_group_post_id').value;
  166. FL_TXT.searchGroup(search_string, group_id, group_post_id);
  167. break;
  168. };
  169. };
  170.  
  171. FL_TXT.searchGroup = function (str, group_id, group_post_id) {
  172. var url = 'https://fetlife.com/groups/' + group_id;
  173. if (group_post_id) {
  174. url += '/group_posts/' + group_post_id;
  175. }
  176. FL_TXT.getPageFromURL(url);
  177. };
  178.  
  179. FL_TXT.displayResult = function (el, pos, url) {
  180. var ul = document.getElementById('fetlife_text_search_results');
  181. var li = document.createElement('li');
  182.  
  183. var icon = el.querySelector('.avatar');
  184. var nick = el.querySelector('.nickname');
  185. var permalink = url;
  186. if (el.getAttribute('id')) {
  187. url += '#' + el.getAttribute('id');
  188. }
  189.  
  190. var start = (100 > pos) ? 0 : pos - 100;
  191. var end = pos + 100;
  192. var html_string = '<a href="' + permalink + '" class="permalink">' + icon.innerHTML + ': ' + el.textContent.substring(start, end) + '</a>';
  193.  
  194. li.innerHTML = html_string;
  195. ul.appendChild(li);
  196. };
  197.  
  198. // This is the main() function, executed on page load.
  199. FL_TXT.main = function () {
  200. // Get relevant object IDs.
  201. var group_id, group_post_id;
  202. var m = window.location.href.match(/https:\/\/fetlife\.com\/groups\/(\d+)(\/group_posts\/(\d+))?/);
  203. if (!m) {
  204. FL_TXT.log('Failed to find FetLife Group ID or group post ID in ' + window.location.href);
  205. return false;
  206. } else {
  207. group_id = m[1];
  208. group_post_id = m[3];
  209. }
  210.  
  211. // Insert FetLife Text Search button interface.
  212. var li = document.createElement('li');
  213. li.setAttribute('id', 'tab-text-search');
  214. var a = document.createElement('a');
  215. a.setAttribute('href', '#');
  216. a.addEventListener('click', FL_TXT.toggleSearchInterface);
  217. a.innerHTML = 'Search ' + this.getGroupName();
  218. li.appendChild(a);
  219.  
  220. var div = document.createElement('div');
  221. var form = document.createElement('form');
  222. form.setAttribute('action', window.location.href)
  223. form.setAttribute('method', 'GET');
  224. form.addEventListener('submit', FL_TXT.doTextSearch);
  225. var html_string = '<input type="hidden" id="fetlife_text_search_group_id" name="fetlife_text_search_group_id" value="' + group_id + '" />';
  226. html_string += '<label><input type="text" id="fetlife_text_search" name="fetlife_text_search" value="" placeholder="Search for&hellip;"/></label>';
  227. html_string += '<input type="submit" name="fetlife_text_search_discussions" value="Search all discussions." />';
  228. // If this is a specific discussion, offer to search only within that discussion thread.
  229. if (group_post_id) {
  230. html_string += '<input type="submit" name="fetlife_text_search_this_thread" value="Search this thread." />';
  231. html_string += '<input type="hidden" id="fetlife_text_search_group_post_id" name="fetlife_text_search_group_post_id" value="' + group_post_id + '" />';
  232. }
  233. form.innerHTML = html_string;
  234.  
  235. div.appendChild(form);
  236. li.appendChild(div);
  237. document.getElementById('tabnav').appendChild(li);
  238.  
  239. var ul = document.createElement('ul');
  240. ul.setAttribute('id', 'fetlife_text_search_results');
  241. div.appendChild(ul);
  242. };
  243.  
  244. // The following is required for Chrome compatibility, as we need "text/html" parsing.
  245. /*
  246. * DOMParser HTML extension
  247. * 2012-09-04
  248. *
  249. * By Eli Grey, http://eligrey.com
  250. * Public domain.
  251. * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
  252. */
  253.  
  254. /*! @source https://gist.github.com/1129031 */
  255. /*global document, DOMParser*/
  256.  
  257. (function(DOMParser) {
  258. "use strict";
  259.  
  260. var
  261. DOMParser_proto = DOMParser.prototype
  262. , real_parseFromString = DOMParser_proto.parseFromString
  263. ;
  264.  
  265. // Firefox/Opera/IE throw errors on unsupported types
  266. try {
  267. // WebKit returns null on unsupported types
  268. if ((new DOMParser).parseFromString("", "text/html")) {
  269. // text/html parsing is natively supported
  270. return;
  271. }
  272. } catch (ex) {}
  273.  
  274. DOMParser_proto.parseFromString = function(markup, type) {
  275. if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
  276. var
  277. doc = document.implementation.createHTMLDocument("")
  278. ;
  279.  
  280. doc.body.innerHTML = markup;
  281. return doc;
  282. } else {
  283. return real_parseFromString.apply(this, arguments);
  284. }
  285. };
  286. }(DOMParser));