# GRO Index Search Helper

Adds additional functionality to the UK General Register Office (GRO) BMD index search

当前为 2016-11-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name # GRO Index Search Helper
  3. // @description Adds additional functionality to the UK General Register Office (GRO) BMD index search
  4. // @namespace cuffie81.scripts
  5. // @include https://www.gro.gov.uk/gro/content/certificates/indexes_search.asp
  6. // @version 1.4
  7. // @grant none
  8. // @require https://code.jquery.com/jquery-2.2.4.min.js
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js
  10. // ==/UserScript==
  11.  
  12. /*
  13. ======================INLINE_RESOURCE_BEGIN======================
  14. ***********RESOURCE_START=Template-EW_Birth*************
  15. <style type="text/css">
  16. div[results-view='EW_Birth'] td
  17. {
  18. padding: 5px 3px;
  19. font-size: 75%;
  20. color: #663333;
  21. vertical-align: top;
  22. }
  23. div[results-view='EW_Birth'] thead td
  24. {
  25. font-weight: bold;
  26. }
  27. div[results-view='EW_Birth'] tbody tr:nth-child(4n+1),
  28. div[results-view='EW_Birth'] tbody tr:nth-child(4n+2)
  29. {
  30. background-color: #F9E8A5;
  31. }
  32. div[results-view='EW_Birth'] tr.rec-actions a
  33. {
  34. padding: 0px 5px;
  35. font-size: 90%;
  36. color: #663333;
  37. text-decoration: none;
  38. }
  39. </style>
  40. <div results-view='EW_Birth' style='display: none; margin-bottom: 25px'>
  41. <table style='width: 100%; border-collapse: collapse'>
  42. <thead>
  43. <tr>
  44. <td class='main_text' style='padding: 5px 3px; font-weight: bold; width: 12%'>Date</td>
  45. <td>Name</td>
  46. <td>Mother</td>
  47. <td style='max-width: 30%'>District</td>
  48. <td>Vol</td>
  49. <td>Page</td>
  50. </tr>
  51. </thead>
  52. <tbody>
  53. {{#each items}}
  54. <tr class='rec'>
  55. <td>{{year}} Q{{quarter}}</td>
  56. <td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span></td>
  57. <td>{{mother}}</td>
  58. <td>{{district}}</td>
  59. <td>{{volume}}</td>
  60. <td>{{page}}</td>
  61. </tr>
  62. <tr class='rec-actions' style='display: none'>
  63. <td colspan='6' style='text-align: right'>
  64. {{#actions}}
  65. <a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
  66. {{/actions}}
  67. </td>
  68. </tr>
  69. {{/each}}
  70. </tbody>
  71. </table>
  72. </div>
  73. *************RESOURCE_END*************
  74.  
  75. ***********RESOURCE_START=Template-EW_Death*************
  76. <style type="text/css">
  77. div[results-view='EW_Death'] td
  78. {
  79. padding: 5px 3px;
  80. font-size: 75%;
  81. color: #663333;
  82. vertical-align: top;
  83. }
  84. div[results-view='EW_Death'] thead td
  85. {
  86. font-weight: bold;
  87. }
  88. div[results-view='EW_Death'] tbody tr:nth-child(4n+1),
  89. div[results-view='EW_Death'] tbody tr:nth-child(4n+2)
  90. {
  91. background-color: #F9E8A5;
  92. }
  93. div[results-view='EW_Death'] tr.rec-actions a
  94. {
  95. padding: 0px 5px;
  96. font-size: 90%;
  97. color: #663333;
  98. text-decoration: none;
  99. }
  100. </style>
  101. <div results-view='EW_Death' style='display: none; margin-bottom: 25px'>
  102. <table style='width: 100%; border-collapse: collapse'>
  103. <thead>
  104. <tr>
  105. <td style='width: 12%'>Date</td>
  106. <td>Name</td>
  107. <td>Age{{#if ageCautionThreshold}}*{{/if}}</td>
  108. <td>Birth</td>
  109. <td style='max-width: 30%'>District</td>
  110. <td>Vol</td>
  111. <td>Page</td>
  112. </tr>
  113. </thead>
  114. <tbody>
  115. {{#each items}}
  116. <tr class='rec'>
  117. <td>{{year}} Q{{quarter}}</td>
  118. <td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span></td>
  119. <td>{{age}}{{#if ageCaution}}*{{/if}}</td>
  120. <td>{{birth}}
  121. <td>{{district}}</td>
  122. <td>{{volume}}</td>
  123. <td>{{page}}</td>
  124. </tr>
  125. <tr class='rec-actions' style='display: none'>
  126. <td colspan='7' style='text-align: right'>
  127. {{#actions}}
  128. <a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
  129. {{/actions}}
  130. </td>
  131. </tr>
  132. {{/each}}
  133. </tbody>
  134. </table>
  135. {{#if ageCautionThreshold}}
  136. <p class='main_text'>* Ages are assumed to be years but <i>may</i> be months. Ages below {{ageCautionThreshold}} should be treated with caution.</p>
  137. {{/if}}
  138. </div>
  139. *************RESOURCE_END*************
  140.  
  141. ======================INLINE_RESOURCE_END======================
  142. */
  143.  
  144. this.$ = this.jQuery = jQuery.noConflict(true);
  145.  
  146. $(function()
  147. {
  148. var initialiseSearchForm = function()
  149. {
  150. // Hide the reset button
  151. $("form[name='SearchIndexes'] input[type='submit'][value='Reset']").hide();
  152.  
  153. // Hide superfluous text
  154. $("table[summary*='contains the search form fields'] > tbody > tr:nth-of-type(2)").hide();
  155. $("table[summary*='contains the search form fields'] > tbody > tr:nth-of-type(3) td.main_text[colspan='5']").parent().hide();
  156.  
  157. // Add gender and year navigation buttons, and style them
  158. var searchButton = $("form[name='SearchIndexes'] input[type='submit'][value='Search']")
  159.  
  160. $("<input class='formButton' id='groish_BtnGenderToggle' type='button' value='Gender' />").insertBefore($(searchButton));
  161. $("<input class='formButton' id='groish_BtnYearsPrev' type='button' value='&lt; Years' />").insertBefore($(searchButton));
  162. $("<input class='formButton' id='groish_BtnYearsNext' type='button' value='Years &gt;' />").insertBefore($(searchButton));
  163.  
  164. var buttonContainer = $("form[name='SearchIndexes'] input[type='submit'][value='Search']").closest("td");
  165. $(buttonContainer).css("padding-bottom", "10px");
  166. $(buttonContainer).find("input[type='button']").css("margin-right", "20px");
  167. $(buttonContainer).find("input[type='submit'], input[type='button']").css("min-width", "100px").css("font-size", "13px").css("padding", "4px 10px");
  168.  
  169. // Add button event handlers
  170. $("input#groish_BtnYearsPrev").click(function() { navigateYears(false); });
  171. $("input#groish_BtnYearsNext").click(function() { navigateYears(true); });
  172. $("input#groish_BtnGenderToggle").click(function() { toggleGender(); });
  173. }
  174. var initialiseResultViews = function(recordType, resources)
  175. {
  176. //console.log("start: initialiseResultViews");
  177. // Move default results table into a view container
  178. var defaultTable = $("form[name='SearchIndexes'] h3:contains('Results:')").closest("table").css("width", "100%").addClass("groish_ResultsTable");
  179. $(defaultTable).before($("<div results-view='default' />"));
  180. var defaultView = $("div[results-view='default']");
  181. $(defaultView).append($("table.groish_ResultsTable"));
  182.  
  183. // Move header row to before default view
  184. $(defaultView).before($("<div class='groish_ResultsHeader' style='margin: 10px 0px; position: relative' />"));
  185. $(".groish_ResultsHeader").append($("table.groish_ResultsTable h3:contains('Results:')"));
  186.  
  187. // Move pager row contents to after default view
  188. $(defaultView).after($("table.groish_ResultsTable > tbody > tr:last table:first"));
  189. $("div[results-view='default'] + table").css("width", "100%").addClass("groish_ResultsInfo");
  190.  
  191.  
  192. // Add alternate view
  193. var results = getResults(recordType);
  194. //console.log(results);
  195. if (results != null && recordType && results.items != null && results.items.length > 0)
  196. {
  197. // Get template and add alternate view
  198. var template = resources["Template-" + recordType].toString();
  199. var compiledTemplate = Handlebars.compile(template);
  200. var html = compiledTemplate(results);
  201. $(defaultView).after($(html));
  202.  
  203. // Add event handler to hide/show actions row
  204. // TODO: Make adding view event handlers more dynamic, so they can be specific to the view
  205. $("div[results-view][results-view!='default'] tbody tr.rec").click(function(index)
  206. {
  207. $(this).next("tr.rec-actions:not(:empty)").toggle();
  208. });
  209.  
  210. // Add click handlers to results header, to toggle views
  211. $(".groish_ResultsHeader").append($("<div id='groish_ViewSwitcher' class='main_text' style='display:inline-block; position: absolute; color: #993333; font-weight: bold; right: 10px; bottom: 0px; cursor: pointer'>Switch view</div>"))
  212. .click(function() { switchResultsView(); });
  213. //$("form[name='SearchIndexes'] h3:contains('Results:')").click(function() { switchResultsView(); });
  214.  
  215. // Show the last used view
  216. var viewName = sessionStorage.getItem("groish_view." + recordType);
  217. //console.log("initialising view: %s", viewName);
  218. if (viewName && $("div[results-view='" + viewName + "']:hidden").length == 1)
  219. {
  220. //console.log("setting active view: %s", viewName);
  221. $("div[results-view][results-view!='" + viewName + "']").hide();
  222. $("div[results-view][results-view='" + viewName + "']").show();
  223. }
  224. }
  225. //console.log("end: initialiseResultViews");
  226. }
  227. var switchResultsView = function()
  228. {
  229. //console.log("switchResultsView");
  230. var recordType = getRecordType();
  231. var views = $("div[results-view]");
  232. if (views.length > 1)
  233. {
  234. var curIndex = -1;
  235. $(views).each(function(index)
  236. {
  237. if ($(this).css("display") != "none")
  238. curIndex = index;
  239. });
  240.  
  241. //console.log("current view index: %s", curIndex);
  242. if (curIndex !== -1)
  243. {
  244. var newIndex = ((curIndex == (views.length-1)) ? 0 : curIndex+1);
  245. $(views).hide();
  246. $("div[results-view]:eq(" + newIndex + ")").show();
  247.  
  248. // Get the name and save it
  249. var viewName = $("div[results-view]:eq(" + newIndex + ")").attr("results-view")
  250. sessionStorage.setItem("groish_view." + recordType, viewName); //save it
  251. //console.log("new view: %s", viewName);
  252. }
  253. }
  254. }
  255. var getResults = function(recordType)
  256. {
  257. //console.log("start: getResults");
  258. var results = { "ageCautionThreshold": 24, "items": [] };
  259. // Lookup record type - birth or death
  260. if (recordType !== null && (recordType === "EW_Birth" || recordType === "EW_Death"))
  261. {
  262. $("div[results-view='default'] > table > tbody > tr")
  263. .has("img[src='./graphics/order_certificate_button.gif']")
  264. .each(function(index)
  265. {
  266. // Get names and reference
  267. var names = $(this).find("td:eq(0)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim();
  268. var ref = $(this).next().find("td:eq(0)").text();
  269.  
  270. // Clean up reference
  271. ref = ref.replace(/\u00a0/g, " ");
  272. ref = ref.replace(/\s\s+/g, ' ');
  273. ref = ref.replace(/GRO Reference: /g, "");
  274. ref = ref.replace(/M Quarter in/g, "Q1");
  275. ref = ref.replace(/J Quarter in/g, "Q2");
  276. ref = ref.replace(/S Quarter in/g, "Q3");
  277. ref = ref.replace(/D Quarter in/g, "Q4");
  278.  
  279. var age = 0;
  280. if (recordType === "EW_Death")
  281. {
  282. var ageArr = /^([0-9]{1,3})$/.exec($(this).find("td:eq(1)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim());
  283. if (ageArr)
  284. age = parseInt(ageArr[1], 10);
  285. }
  286. var mother = null;
  287. if (recordType === "EW_Birth")
  288. mother = toTitleCase($(this).find("td:eq(1)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ')).trim();
  289. var actions = [];
  290. var orderCertUrl = $(this).find("a[href^='indexes_order.asp']:eq(0)").prop("href");
  291. var orderPdfUrl = $(this).next().find("a[href^='indexes_order.asp']:eq(0)").prop("href");
  292.  
  293. if (orderCertUrl) actions.push( {"text": "Order Certificate", "url": orderCertUrl });
  294. if (orderPdfUrl) actions.push( {"text": "Order Research Copy", "title": "PDF", "url": orderPdfUrl });
  295.  
  296. // Parse forenames, surname, year, quarter, district, vol, page
  297. var namesArr = /([a-z' -]+),([a-z' -]*)/gi.exec(names);
  298. var refArr = /([0-9]{4}) Q([1-4]) ([a-z\.\-,\(\)0-9\&' ]*)Volume ([a-z0-9]+) Page ([0-9]+)/gi.exec(ref); // NB: the district may not be set in some cases
  299. //console.log("index: %d, namesArr: %s, refArr: %s", index, namesArr, refArr);
  300.  
  301. if (namesArr !== null && refArr !== null)
  302. {
  303. var forenames = toTitleCase(namesArr[2]).trim();
  304. var surname = toTitleCase(namesArr[1]).trim();
  305. var year = parseInt(refArr[1], 10);
  306. var quarter = parseInt(refArr[2], 10);
  307. var district = toTitleCase(refArr[3]).trim();
  308. var volume = refArr[4].toLowerCase();
  309. var page = refArr[5];
  310.  
  311. //console.log("forenames: %s, surname: %s, age: %d, year: %s, quarter: %s, district: %s, vol: %s, page: %s", forenames, surname, age, year, quarter, district, volume, page);
  312. var record =
  313. {
  314. "forenames": forenames,
  315. "surname": surname,
  316. "age": age,
  317. "ageCaution": (age != null && age > 0 && age <= results.ageCautionThreshold),
  318. "birth": (age != null ? year - age : null),
  319. "mother": mother,
  320. "year": year,
  321. "quarter": quarter,
  322. "district": district,
  323. "volume": volume,
  324. "page": page,
  325. "actions": actions
  326. };
  327.  
  328. results.items.push(record);
  329. //console.log(record);
  330. }
  331. else
  332. {
  333. //console.log("Failed to read record: %d", index);
  334. }
  335. });
  336. }
  337.  
  338. // Sort records
  339. if (results.items.length > 0)
  340. {
  341. results.items.sort(function(a, b)
  342. {
  343. if (a.year == b.year && a.quarter == b.quarter)
  344. return 0;
  345. else if ((a.year > b.year) || (a.year == b.year && a.quarter > b.quarter))
  346. return 1;
  347. else
  348. return -1;
  349. });
  350. }
  351. //console.log("end: getResults");
  352.  
  353. return results;
  354. }
  355.  
  356.  
  357. var toTitleCase = function(str)
  358. {
  359. return str.replace(/([^\W_]+[^\s-]*) */g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
  360. }
  361.  
  362. var toggleGender = function()
  363. {
  364. var curGender = $("form[name='SearchIndexes'] select#Gender").val();
  365. $("form[name='SearchIndexes'] select#Gender").val((curGender === "F" ? "M" : "F"));
  366. $("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
  367. }
  368.  
  369. var navigateYears = function(forward)
  370. {
  371. // Get min and max years
  372. var minYear = parseInt($("form[name='SearchIndexes'] select#Year option:eq(2)").val(), 10);
  373. var maxYear = parseInt($("form[name='SearchIndexes'] select#Year option:last").val(), 10);
  374.  
  375. //console.log("Year range: %s - %s", minYear, maxYear);
  376.  
  377. if (!isNaN(minYear) && !isNaN(maxYear))
  378. {
  379. // Read current year and range
  380. var curYear = parseInt($("form[name='SearchIndexes'] select#Year").val(), 10);
  381. var curRange = parseInt($("form[name='SearchIndexes'] select#Range").val(), 10);
  382.  
  383. if (!isNaN(curYear) && !isNaN(curRange))
  384. {
  385. // Calculate the new year
  386. var step = (curRange * 2) + 1;
  387. var newYear = (forward ? curYear+step : curYear-step);
  388. newYear = Math.min(Math.max(newYear, minYear), maxYear);
  389.  
  390. // Update the year and submit the search
  391. $("form[name='SearchIndexes'] select#Year").val(newYear);
  392. $("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
  393. }
  394.  
  395. //console.log("Current year: %d +-%d (%d-%d), New year: %d (%d-%d)", curYear, curRange, curYear-curRange, curYear+curRange, newYear, newYear-curRange, newYear+curRange);
  396. }
  397. }
  398. var getRecordType = function()
  399. {
  400. return $("form[name='SearchIndexes'] input[type='radio'][name='index']:checked").val();
  401. }
  402.  
  403. // https://gist.github.com/aidanhs/5534196
  404. var getInlineResources = function()
  405. {
  406. var resource = {}, len, match, resourceBlocks, inlineResourcesMatch = (/^=+INLINE_RESOURCE_BEGIN=+$([\s\S]*?)^=+INLINE_RESOURCE_END=+$/m).exec(GM_info.scriptSource);
  407. resourceBlocks = (inlineResourcesMatch && inlineResourcesMatch[1].match(/^\**RESOURCE_START[\s\S]*?^\**RESOURCE_END\**$/mg)) || null;
  408. len = (resourceBlocks && resourceBlocks.length) || 0;
  409.  
  410. for (var i = 0; i < len; i++)
  411. {
  412. match = (/^\**RESOURCE_START=(.*?)\**$\s*^([\s\S]*)^\**RESOURCE_END\**$/m).exec(resourceBlocks[i]);
  413. resource[match[1]] = match[2];
  414. }
  415.  
  416. return resource;
  417. }
  418.  
  419. // Get the ball rolling...
  420. var resources = getInlineResources();
  421. var recordType = getRecordType();
  422. initialiseSearchForm();
  423. initialiseResultViews(recordType, resources);
  424. });
  425.  
  426.