GRO Index Search Helper

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

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

  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. // @match https://www.gro.gov.uk/gro/content/certificates/indexes_search.asp*
  6. // @version 1.18
  7. // @grant GM_listValues
  8. // @grant GM_getValue
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.2.0/handlebars.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js
  12. // ==/UserScript==
  13.  
  14.  
  15. this.$ = this.jQuery = jQuery.noConflict(true);
  16.  
  17. $(function() {
  18. let resources, recordType, results;
  19. var main = function() {
  20. // Register Handlebars helper operators
  21. Handlebars.registerHelper({
  22. eq: function (v1, v2) { return v1 === v2; },
  23. ne: function (v1, v2) { return v1 !== v2; },
  24. lt: function (v1, v2) { return v1 < v2; },
  25. gt: function (v1, v2) { return v1 > v2; },
  26. lte: function (v1, v2) { return v1 <= v2; },
  27. gte: function (v1, v2) { return v1 >= v2; },
  28. and: function () { return Array.prototype.slice.call(arguments).every(Boolean); },
  29. or: function () { return Array.prototype.slice.call(arguments, 0, -1).some(Boolean); }
  30. });
  31. buildResources();
  32. recordType = getRecordType();
  33. //console.log("resources:\r\n%s", JSON.stringify(resources));
  34. // Load the general css
  35. $("body").append($(resources.baseStyle));
  36.  
  37. initialiseSearchForm();
  38. initialiseResultViews();
  39. // Scroll down to the form. Do this last as we may add/remove/change elements in the previous calls.
  40. $("h1:contains('Search the GRO Online Index')")[0].scrollIntoView();
  41. // Wire up accesskeys to clicks, to avoid having to use the full accesskey combo (eg ALT+SHFT+#)
  42. $(document).on("keypress", function(e) {
  43. if (!document.activeElement || document.activeElement.tagName.toLowerCase() !== "input")
  44. {
  45. let char = String.fromCharCode(e.which);
  46. //console.log("keypress: %s", char);
  47. if ($("*[id^='groish'][accesskey='" + char + "']").length)
  48. $("*[id^='groish'][accesskey='" + char + "']").click();
  49. else if (char == "{")
  50. adjustSearchYear(-10);
  51. else if (char == "}")
  52. adjustSearchYear(10);
  53. else if (char == "?")
  54. $("form[name='SearchIndexes'] input[type='submit']").click();
  55. else if (char == '@')
  56. switchRecordType();
  57. }
  58. });
  59. }
  60. var initialiseSearchForm = function() {
  61. // Hide superfluous spacing, text and buttons
  62. $("form[name='SearchIndexes'] input[type='submit'][value='Reset']").hide();
  63. $("form[name='SearchIndexes'] a.tooltip").hide();
  64. $("form[name='SearchIndexes'] span.main_text").has("i > a[href^='most_customers_want_to_know.asp']").hide();
  65. $("form[name='SearchIndexes'] a:contains('FreeBMD')").hide();
  66. $("form[name='SearchIndexes'] a:contains('(used 1837')").hide();
  67.  
  68. $("form[name='SearchIndexes'] td.main_text[colspan='5'] > br").closest("tr").hide();
  69. $("form[name='SearchIndexes'] td.main_text[colspan='5'] > strong").closest("tr").hide();
  70. $("form[name='SearchIndexes'] #SurnameMatchesText").closest("tr").hide();
  71. $("form[name='SearchIndexes'] #ForenameMatchesText").closest("tr").hide();
  72. $("form[name='SearchIndexes'] #MothersMaidenSurnameMatchesText").closest("tr").hide();
  73. // Change text
  74. $("form[name='SearchIndexes'] td span.main_text:contains('year(s)')").text("yrs");
  75. $("form[name='SearchIndexes'] td.main_text:contains('Surname at Death:')").html("Surname:<span class='redStar'>*</span>");
  76. $("form[name='SearchIndexes'] td.main_text:contains('First Forename at Death:')").text("Forename 1:");
  77. $("form[name='SearchIndexes'] td.main_text:contains('Second Forename at Death:')").text("Forename 2:");
  78. $("form[name='SearchIndexes'] td.main_text:contains('District of Death:')").text("District:");
  79. $("form[name='SearchIndexes'] td.main_text:contains('Age at'):contains('Death'):contains('in years')").text("Age:");
  80. $("form[name='SearchIndexes'] td.main_text:contains('Surname at Birth:')").html("Surname:<span class='redStar'>*</span>");
  81. $("form[name='SearchIndexes'] td.main_text:contains('First Forename:')").text("Forename 1:");
  82. $("form[name='SearchIndexes'] td.main_text:contains('Second Forename:')").text("Forename 2:");
  83. $("form[name='SearchIndexes'] td.main_text:contains('Maiden Surname:')").text("Mother:");
  84. $("form[name='SearchIndexes'] td.main_text:contains('District of Birth:')").text("District:");
  85. $("form[name='SearchIndexes'] td.main_text:contains('Register No:')").text("Register:");
  86. $("form[name='SearchIndexes'] td.main_text:contains('Entry'):contains('No:')").text("Entry:");
  87. $("form[name='SearchIndexes'] a:contains('View list of registration districts')").text("Districts");
  88.  
  89. // Add gender and year navigation buttons, and style them
  90. let searchButton = $("form[name='SearchIndexes'] input[type='submit'][value='Search']");
  91. $(searchButton).attr("accesskey", "?");
  92. $(searchButton).parent().find("br").remove();
  93.  
  94. $("<input type='button' class='formButton' accesskey='#' id='groish_BtnToggleGender' value='Gender' />").insertBefore($(searchButton));
  95. $("<input type='button' class='formButton' accesskey='[' id='groish_BtnYearsPrev' value='&lt; Years' />").insertBefore($(searchButton));
  96. $("<input type='button' class='formButton' accesskey=']' id='groish_BtnYearsNext' value='Years &gt;' />").insertBefore($(searchButton));
  97. let buttonContainer = $("form[name='SearchIndexes'] input[type='submit'][value='Search']").closest("td").addClass("groish_ButtonContainer");
  98. // Add button event handlers
  99. $("input#groish_BtnYearsPrev").click(function() { navigateYears(false); });
  100. $("input#groish_BtnYearsNext").click(function() { navigateYears(true); });
  101. $("input#groish_BtnToggleGender").click(function() { toggleGender(); });
  102.  
  103. // Set encoding
  104. if (typeof $("form[name='SearchIndexes']").attr("accept-charset") === typeof undefined) {
  105. $("form[name='SearchIndexes']").attr("accept-charset", "UTF-8");
  106. }
  107.  
  108. }
  109. var initialiseResultViews = function() {
  110. // Move default results table into a view container
  111. let defaultTable = $("form[name='SearchIndexes'] h3:contains('Results:')").closest("table").css("width", "100%").addClass("groish_ResultsTable");
  112. $(defaultTable).before($("<div results-view='default' />"));
  113. let defaultView = $("div[results-view='default']");
  114. $(defaultView).append($("table.groish_ResultsTable"));
  115.  
  116. // Move header row to before default view
  117. $(defaultView).before($("<div class='groish_ResultsHeader' style='margin: 10px 0px; position: relative' />"));
  118. $(".groish_ResultsHeader").append($("table.groish_ResultsTable h3:contains('Results:')"));
  119.  
  120. // Move pager row contents to after default view
  121. $(defaultView).after($("table.groish_ResultsTable > tbody > tr:last table:first"));
  122. $("div[results-view='default'] + table").css("width", "100%").addClass("groish_ResultsInfo");
  123.  
  124. // Get results, sort them and populate views
  125. results = getResults(recordType);
  126. sortResults();
  127. populateAlternateViews();
  128. }
  129. var sortResults = function(reverse, sortFieldsCsv) {
  130. //console.log("sorting results, sort fields: %s", sortFieldsCsv);
  131. if (!results || !results.items)
  132. return;
  133. let defaultSortFields = "year,quarter";
  134. // Get the last sort fields and order for the record type
  135. let sortFieldsKey = recordType + "-sort-fields";
  136. let sortOrderKey = recordType + "-sort-order";
  137. let lastSortFields = sessionStorage.getItem(sortFieldsKey);
  138. let lastSortOrder = sessionStorage.getItem(sortOrderKey);
  139. // Cleanup values
  140. sortFieldsCsv = (sortFieldsCsv || "").replace(/\s\s+/g, ' ');
  141. lastSortFields = (lastSortFields || "").replace(/\s\s+/g, ' ');
  142. //console.log("last sort fields: %s; last sort order: %s", lastSortFields, lastSortOrder);
  143. let sortOrder = "asc";
  144. if (!sortFieldsCsv) {
  145. sortFieldsCsv = lastSortFields || defaultSortFields;
  146. sortOrder = lastSortOrder || "asc";
  147. }
  148. else if (sortFieldsCsv.localeCompare(lastSortFields) == 0 && sortOrder.localeCompare(lastSortOrder) == 0 && reverse) {
  149. sortOrder = "desc";
  150. }
  151. // Build sort fields and order arrays
  152. let sortFields = sortFieldsCsv.split(",");
  153. let sortOrders = Array.apply(null, Array(sortFields.length)).map(String.prototype.valueOf, sortOrder);
  154. // Append defaults if needed
  155. if (sortFieldsCsv.localeCompare(defaultSortFields) != 0) {
  156. sortFields.push("year");
  157. sortFields.push("quarter");
  158. sortOrders.push("asc");
  159. sortOrders.push("asc");
  160. }
  161. //console.log("sorting results by: %s (%s)", sortFields, sortOrders);
  162. results.items = _.orderBy(results.items, sortFields, sortOrders);
  163. sessionStorage.setItem(sortFieldsKey, sortFieldsCsv);
  164. sessionStorage.setItem(sortOrderKey, sortOrder);
  165. }
  166.  
  167. var populateAlternateViews = function() {
  168. // Add alternate view(s)
  169. if (recordType && resources && results && results.items && results.items.length > 0) {
  170. // Remove any existing views
  171. $("div[results-view][results-view!='default']").remove();
  172. // Add alternate views
  173. //console.log("Adding alternate views...");
  174. let viewPrefix = "view_" + recordType; // record type = EW_Birth, EW_Death
  175. for (let resourceName in resources) {
  176. let resourceNamePrefix = resourceName.substring(0, viewPrefix.length);
  177. if (resources.hasOwnProperty(resourceName) && viewPrefix.localeCompare(resourceNamePrefix) == 0) {
  178. let template = resources[resourceName].toString();
  179. let compiledTemplate = Handlebars.compile(template);
  180. let html = compiledTemplate(results);
  181. if (html) {
  182. $("div[results-view]").filter(":last").after($(html));
  183. //console.log("Added alternate view");
  184. }
  185. }
  186. }
  187. // Add view helpers and event handlers, if not already added
  188. if ($("div[results-view]").length > 1) {
  189. // Add event handler to hide/show actions row
  190. // TODO: Make adding view event handlers more dynamic, so they can be specific to the view
  191. $("div[results-view][results-view!='default'] tbody tr.rec")
  192. .off("click.groish")
  193. .on("click.groish", function(event) {
  194.  
  195. event.preventDefault();
  196. $(this).next("tr.rec-actions:not(:empty)").toggle();
  197. }
  198. );
  199.  
  200. // Add event handler for column sorting
  201. $("div[results-view][results-view!='default'] thead td[sort-fields]")
  202. .off("click.groish")
  203. .on("click.groish", function(event) {
  204.  
  205. event.preventDefault();
  206. //let defaultSortFields = ($(this).closet("div[results-view]").attr("default-sort-fields");
  207. let sortFields = ($(this).attr("sort-fields") ? $(this).attr("sort-fields") : $(this).text());
  208. sortResults(true, sortFields);
  209. populateAlternateViews();
  210. }
  211. );
  212.  
  213. // Add view switcher, if it doesn't already exist
  214. if ($("#groish_ViewSwitcher").length == 0) {
  215. $(".groish_ResultsHeader").append($("<a href='#' id='groish_ViewSwitcher' class='main_text' accesskey='~'>Switch view</a>"));
  216. $("#groish_ViewSwitcher").off("click.groish").on("click.groish", function() { switchResultsView(); return false; });
  217.  
  218.  
  219. // Add results copier (if supported)
  220. if (window.getSelection && document.createRange) {
  221. $(".groish_ResultsHeader").append($("<a href='#' id='groish_ResultsCopier' class='main_text' accesskey='|'>Copy results</a>"));
  222. $("#groish_ResultsCopier")
  223. .off("click.groish")
  224. .on("click.groish", function(event) {
  225.  
  226. event.preventDefault();
  227.  
  228. // Get most specific element containing results, typically a table body
  229. let resultsContent = $("div[results-view]:visible tbody");
  230.  
  231. if (resultsContent.length == 0)
  232. resultsContent = $("div[results-view]:visible");
  233. if (resultsContent.length > 0) {
  234. resultsContent = resultsContent[0];
  235. let selection = window.getSelection();
  236. let range = document.createRange();
  237. range.selectNodeContents(resultsContent);
  238. selection.removeAllRanges();
  239. selection.addRange(range);
  240.  
  241. try {
  242. if (document.execCommand("copy")) {
  243. selection.removeAllRanges();
  244. $(".groish_Message").text("Results copied to clipboard").show();
  245. setTimeout(function() { $(".groish_Message").fadeOut(); }, 3000);
  246. }
  247. }
  248. catch(e) { }
  249. }
  250.  
  251. return false;
  252. });
  253. }
  254. }
  255. }
  256.  
  257. // Show the last used view
  258. let viewName = sessionStorage.getItem("groish_view." + recordType);
  259. //console.log("initialising view: %s", viewName);
  260. if (viewName && $("div[results-view='" + viewName + "']:hidden").length == 1) {
  261. //console.log("setting active view: %s", viewName);
  262. $("div[results-view][results-view!='" + viewName + "']").hide();
  263. $("div[results-view][results-view='" + viewName + "']").show();
  264. }
  265. }
  266. }
  267.  
  268. var switchResultsView = function() {
  269. let views = $("div[results-view]");
  270. if (views.length > 1) {
  271. let curIndex = -1;
  272. $(views).each(function(index) {
  273. if ($(this).css("display") != "none")
  274. curIndex = index;
  275. });
  276.  
  277. //console.log("current view index: %s", curIndex);
  278. if (curIndex !== -1) {
  279. let newIndex = ((curIndex == (views.length-1)) ? 0 : curIndex+1);
  280. $(views).hide();
  281. $("div[results-view]:eq(" + newIndex + ")").show();
  282.  
  283. $(".groish_Message").hide();
  284.  
  285. // Get the name and save it
  286. let viewName = $("div[results-view]:eq(" + newIndex + ")").attr("results-view")
  287. sessionStorage.setItem("groish_view." + recordType, viewName); //save it
  288. //console.log("new view: %s", viewName);
  289. }
  290. }
  291. }
  292. var getResults = function(recordType) {
  293. let results = { "ageWarningThreshold": 24, "items": [], "failures": [] };
  294. // Lookup record type - birth or death
  295. if (recordType !== null && (recordType === "EW_Birth" || recordType === "EW_Death")) {
  296. let gender = $("form[name='SearchIndexes'] select#Gender").val();
  297. let year = parseInt($("form[name='SearchIndexes'] select#Year").val(), 10);
  298. let dataFormat = (year >= 1993 ? 1993 : (year >= 1984 ? 1984 : 1837));
  299.  
  300. // Save the data format
  301. results["dataFormat" + dataFormat] = true;
  302. $("div[results-view='default'] > table > tbody > tr")
  303. .has("input[type='radio'][name='SearchResult']")
  304. .each(function(index) {
  305. try
  306. {
  307. //console.log("Parsing record (%d)...", index);
  308. let quarterNames = [ "Mar", "Jun", "Sep", "Dec" ];
  309. // Get result id, contains year and record id
  310. let recordId = null;
  311. let resultId = $(this).find("input[type='radio'][name='SearchResult']:first").val();
  312. if (resultId && resultId.length > 5 && resultId.indexOf('.') == 4)
  313. recordId = resultId.substring(5);
  314. // Get names and reference
  315. let names = $(this).find("td:eq(1)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim();
  316. let ref = $(this).next().find("td:eq(0)").text();
  317.  
  318. // Clean up reference
  319. ref = ref.replace(/\u00a0/g, " ");
  320. ref = ref.replace(/\s\s+/g, ' ');
  321. ref = ref.replace(/GRO Reference: /g, "");
  322. ref = ref.replace(/M Quarter in/g, "Q1");
  323. ref = ref.replace(/J Quarter in/g, "Q2");
  324. ref = ref.replace(/S Quarter in/g, "Q3");
  325. ref = ref.replace(/D Quarter in/g, "Q4");
  326. ref = ref.replace(/Order this entry as a:/g, "");
  327. ref = ref.replace(/Entry Number(:|)/gi, "Entry");
  328. ref = ref.replace(/Occasional Copy(:|)/gi, "Copy");
  329. ref = ref.replace(/^DOR /gi, "");
  330. ref = ref.replace(/ Union /gi, " ");
  331. if (/(((-|Q[1-9])\/[0-9]{4}) in )/gi.test(ref))
  332. ref = ref.replace(/(((-|Q[1-9])\/[0-9]{4}) in )/gi, "$2 ");
  333. ref = ref.replace(/\s\s+/g, ' ');
  334. ref = ref.trim();
  335.  
  336.  
  337. // Parse forenames, surname
  338. let namesArr = /([a-z' -]+),([a-z' -]*)/gi.exec(names);
  339. //console.log("index: %d, namesArr: %s", index, namesArr);
  340. // Parse mother's maiden name
  341. let mother = null;
  342. if (recordType === "EW_Birth")
  343. mother = toTitleCase($(this).find("td:eq(2)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ')).trim();
  344. // Initialise record
  345. let record =
  346. {
  347. "recordId": recordId,
  348. "ref": ref,
  349. "gender": gender,
  350. "forenames": toTitleCase(namesArr[2]).trim(),
  351. "surname": toTitleCase(namesArr[1]).trim(),
  352. "age": null,
  353. "yob": null,
  354. "birth": null,
  355. "mother": mother,
  356. "actions": []
  357. };
  358.  
  359. // Parse reference
  360. // TODO: Use named capture groups when widely supported in browsers
  361. let refPatterns =
  362. [
  363. {
  364. // 1937 Q3 NORTHAMPTON Volume 03B Page 32
  365. "pattern": "([0-9]{4}) Q([1-4]) ([a-z\\.\\-,\\(\\)0-9\\&'\\/ ]*) (Volume ([a-z0-9]+)) (Page ([0-9a-z]+))( Copy ([0-9a-z]+)|)",
  366. "indexes": { "year": 1, "quarter": 2, "district": 3, "volume": 5, "page": 7, "copy": 9 }
  367. },
  368. {
  369. // 1937 Q3 NORTHAMPTON Volume 03B
  370. "pattern": "([0-9]{4}) Q([1-4]) ([a-z\\.\\-,\\(\\)0-9\\&'\\/ ]*) (Volume ([a-z0-9]+))( Copy: ([0-9a-z]+)|)",
  371. "indexes": { "year": 1, "quarter": 2, "district": 3, "volume": 5, "copy": 7 }
  372. },
  373. {
  374. // DOR -/1992 NORTHAMPTON (6701C) Volume 7 Page 2375 Entry Number 126
  375. "pattern": "(-|Q([1-4]))\\/([0-9\\-]{1,4}) ([a-z\\.\\-,\\(\\)0-9\\&'\\/ ]*) (Volume ([a-z0-9]+)) (Page ([0-9a-z]+)) (Entry ([0-9a-z]+))( Copy ([0-9a-z]+)|)",
  376. "indexes": { "quarter": 2, "year": 3, "district": 4, "volume": 6, "page": 8, "entry": 10, "copy": 12 }
  377. },
  378. {
  379. // DOR Q4/1984 NORTHAMPTON (6701B) Volume 7 Page 2456
  380. "pattern": "(-|Q([1-4]))\\/([0-9\\-]{1,4}) ([a-z\\.\\-,\\(\\)0-9\\&'\\/ ]*) (Volume ([a-z0-9]+)) (Page ([0-9a-z]+))( Copy ([0-9a-z]+)|)",
  381. "indexes": { "quarter": 2, "year": 3, "district": 4, "volume": 6, "page": 8, "copy": 10 }
  382. },
  383. {
  384. // DOR Q2/2000 Northampton (6701A) Reg A59B Entry Number 96
  385. "pattern": "(-|Q([1-4]))\\/([0-9\\-]{1,4}) ([a-z\\.\\-,\\(\\)0-9\\&'\\/ ]*) (Reg ([a-z0-9]+)) (Entry ([0-9a-z]+))( Copy ([0-9a-z]+)|)",
  386. "indexes": { "quarter": 2, "year": 3, "district": 4, "reg": 6, "entry": 8, "copy": 10 }
  387. },
  388. {
  389. // DOR Q2/2000 Northampton (6701A) Entry Number 96
  390. "pattern": "(-|Q([1-4]))\\/([0-9\\-]{1,4}) ([a-z\\.\\-,\\(\\)0-9\\&'\\/ ]*) (Entry ([0-9a-z]+))( Copy ([0-9a-z]+)|)",
  391. "indexes": { "quarter": 2, "year": 3, "district": 4, "entry": 6, "copy": 8 }
  392. }
  393. ];
  394. for (let p of refPatterns) {
  395. let re = new RegExp(p.pattern, "gi");
  396. let result = re.exec(ref);
  397. if (result) {
  398. if (p.indexes) {
  399. for (const [key, value] of Object.entries(p.indexes)) {
  400. //console.log("index: %d, name: %s, value: %s", value, key, result[value]);
  401. record[key] = (result && result.length > value && result[value]) ? result[value] : null;
  402. }
  403. }
  404. break;
  405. }
  406. }
  407. // Set format
  408. let recordYear = (record.year ? record.year : year);
  409. let recordDataFormat = (recordYear >= 1993 ? 1993 : (recordYear >= 1984 ? 1984 : 1837));
  410. record["dataFormat"] = recordDataFormat;
  411. results["dataFormat" + recordDataFormat] = true;
  412. // Parse age and year of birth
  413. if (recordType === "EW_Death") {
  414. if (record.dataFormat == 1837) {
  415. let ageArr = /^([0-9]{1,3})$/.exec($(this).find("td:eq(2)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim());
  416. if (ageArr)
  417. record.age = parseInt(ageArr[1], 10);
  418. }
  419. else if (record.dataFormat >= 1984) {
  420. let yobArr = /^([0-9]{4})$/.exec($(this).find("td:eq(2)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim());
  421. if (yobArr)
  422. record.yob = parseInt(yobArr[1], 10);
  423. }
  424. }
  425. // Tidy up data...
  426.  
  427. // Tidy up strings
  428. if (record.district) {
  429. record.district = toTitleCase(record.district);
  430. }
  431. for (let key of [ "forenames", "surname", "district", "volume", "page", "reg", "entry", "copy" ]) {
  432. if (record[key]) {
  433. record[key] = record[key].trim();
  434. }
  435. }
  436. // Tidy up integers
  437. for (let key of [ "age", "yob", "birth", "quarter", "year" ]) {
  438. if (record[key]) {
  439. record[key] = parseInt(record[key], 10);
  440. if (isNaN(record[key]))
  441. record[key] = null;
  442. }
  443. }
  444. // Set calculated data
  445. if (record.yob && record.yob > 0) {
  446. record.birth = record.yob;
  447. if (record.year && record.year > 0)
  448. record.age = record.year - record.yob;
  449. }
  450. else if (record.age != null && record.year && record.year > 0) {
  451. record.birth = record.year - record.age;
  452. }
  453. record.noForenames = (!record.forenames || record.forenames == "-");
  454. record.ageWarning = (record.age && record.age > 0 && record.age <= results.ageWarningThreshold);
  455. record.quarterName = ((record.quarter && record.quarter >=1 && record.quarter <= 4) ? quarterNames[record.quarter-1] : null);
  456.  
  457. //console.log("resultId: %s, record.recordId: %s, record.year: %s, recordType: %s", resultId, record.recordId, record.year, recordType);
  458. // Determine what actions are supported for the record and add them
  459. if (record.recordId && record.year && recordType) {
  460. // Define possible actions
  461. let actions = [
  462. { "text": "Order Certificate", "url": null, "itemType": "Certificate", "pdfStatus": 0, "selector": "img[src$='order_certificate_button.gif']" },
  463. { "text": "Order PDF", "url": null, "itemType": "PDF", "pdfStatus": 5, "selector": "img[src$='order_pdf_button.gif']" }
  464. //{ "text": "Order MSF + Bundle", "url": null, "itemType": "MSFBundle", "pdfStatus": 0, "selector": "img[src$='order_certificate_button.gif']" }
  465. ];
  466. for (let i = 0; i < actions.length; i++) {
  467. if ($(this).next().find(actions[i].selector).length) {
  468. // Build order url
  469. let orderUrl = "https://www.gro.gov.uk/gro/content/certificates/indexes_order.asp?";
  470. orderUrl += "Index=" + recordType;
  471. orderUrl += "&Year=" + record.year;
  472. orderUrl += "&EntryID=" + record.recordId;
  473. orderUrl += "&ItemType=" + actions[i].itemType;
  474. if (actions[i].pdfStatus && actions[i].pdfStatus > 0)
  475. orderUrl += "&PDF=" + actions[i].pdfStatus;
  476. actions[i].url = orderUrl;
  477. record.actions.push(actions[i]);
  478. }
  479. //console.log("action '%s' (%s), url: %s", actions[i].itemType, actions[i].selector, actions[i].url);
  480. }
  481. }
  482. //console.log(record);
  483. results.items.push(record);
  484. }
  485. catch (e)
  486. {
  487. //console.log("Failed to parse record (%d): %s", index, e.message);
  488. results.failures.push({ "index": index, "ex": e });
  489. }
  490. });
  491. }
  492. return results;
  493. }
  494.  
  495. var toTitleCase = function(str) {
  496. return str.replace(/([^\W_]+[^\s-]*) */g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
  497. }
  498. var switchRecordType = function() {
  499. let recordTypes = $("form[name='SearchIndexes'] input[type='Radio'][name='index']");
  500.  
  501. let curIndex = -1;
  502. for (let i = 0; i < recordTypes.length; i++) {
  503. if ($(recordTypes).eq(i).prop("checked")) {
  504. curIndex = i;
  505. break;
  506. }
  507. }
  508. //console.log("current record type: %d", curIndex);
  509.  
  510. if (curIndex >= 0) {
  511. let nextIndex = (curIndex == (recordTypes.length-1)) ? 0 : curIndex + 1;
  512.  
  513. if (nextIndex != curIndex)
  514. $(recordTypes).eq(nextIndex).prop("checked", true).click();
  515. //console.log("next record type: %d", nextIndex);
  516. }
  517. }
  518.  
  519. var toggleGender = function() {
  520. let curGender = $("form[name='SearchIndexes'] select#Gender").val();
  521. $("form[name='SearchIndexes'] select#Gender").val((curGender === "F" ? "M" : "F"));
  522. $("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
  523. }
  524. var adjustSearchYear = function(step) {
  525. let adjusted = false;
  526. // Get min and max years
  527. let minYear = parseInt($("form[name='SearchIndexes'] select#Year option:eq(2)").val(), 10);
  528. let maxYear = parseInt($("form[name='SearchIndexes'] select#Year option:last").val(), 10);
  529.  
  530. //console.log("Year range: %s - %s", minYear, maxYear);
  531.  
  532. if (!isNaN(step) && !isNaN(minYear) && !isNaN(maxYear)) {
  533. // Read current year and range
  534. let curYear = parseInt($("form[name='SearchIndexes'] select#Year").val(), 10);
  535. let curRange = parseInt($("form[name='SearchIndexes'] select#Range").val(), 10);
  536. if (isNaN(curYear) || curYear === 0)
  537. curYear = 1837;
  538.  
  539. if (!isNaN(curRange)) {
  540. // Calculate the new year
  541. let newYear = (isNaN(curYear) ? minYear : curYear+step);
  542. newYear = Math.min(Math.max(newYear, minYear), maxYear);
  543. if (newYear != curYear) {
  544. // Get list of all years
  545. let years = $("form[name='SearchIndexes'] select#Year option[value]")
  546. .toArray()
  547. .map(el => parseInt(el.value, 10))
  548. .filter(y => !isNaN(y) && y > 0)
  549. .sort();
  550. //console.log(years);
  551. // If the year doesn't exist try and find the closest
  552. if (!years.find(y => y === newYear)) {
  553. let stepRange = (Math.abs(step)-1)/2;
  554. let nowYear = new Date().getFullYear();
  555. let minYear = (step > 0) ? newYear - stepRange : 1837;
  556. let maxYear = (step < 0) ? newYear + stepRange : nowYear;
  557. minYear = Math.min(Math.max(minYear, 1837), nowYear);
  558. maxYear = Math.min(Math.max(maxYear, 1837), nowYear);
  559. years = years.filter(y => y >= minYear && y <= maxYear);
  560. newYear = findClosestNumber(years, newYear, minYear, maxYear);
  561. }
  562.  
  563. //console.log("newYear: %d", newYear);
  564. if (newYear && newYear > 0 && newYear != curYear)
  565. {
  566. $("form[name='SearchIndexes'] select#Year").val(newYear);
  567. adjusted = true;
  568. }
  569. }
  570. }
  571.  
  572. //console.log("Current year: %d +-%d (%d-%d), New year: %d (%d-%d)", curYear, curRange, curYear-curRange, curYear+curRange, newYear, newYear-curRange, newYear+curRange);
  573. }
  574.  
  575. return adjusted;
  576. }
  577. var findClosestNumber = function(numbers, target, minNumber, maxNumber) {
  578. //console.log("target: %d, minNumber: %d, maxNumber: %d", target, minNumber, maxNumber);
  579. let number = 0;
  580. if (numbers && target && minNumber && maxNumber && minNumber <= maxNumber) {
  581. target = parseInt(target, 10);
  582. minNumber = parseInt(minNumber, 10);
  583. maxNumber = parseInt(maxNumber, 10);
  584. if (numbers.find(n => n === target)) {
  585. number = target;
  586. }
  587. else {
  588. for (let i = 0; i < numbers.length; i++) {
  589. let n = numbers[i];
  590. if (!isNaN(n) && n >= minNumber && n <= maxNumber && Math.abs(target-n) < Math.abs(target-number)) {
  591. number = n;
  592. if (Math.abs(target-number) == 1)
  593. break;
  594. }
  595. }
  596. }
  597. }
  598. return number;
  599. }
  600.  
  601. var navigateYears = function(forward) {
  602. let curRange = parseInt($("form[name='SearchIndexes'] select#Range").val(), 10);
  603. if (!isNaN(curRange)) {
  604. // Calculate the new year
  605. let step = (curRange * 2) + 1;
  606. if (!forward) step = -step;
  607. if (adjustSearchYear(step)) {
  608. $("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
  609. }
  610. }
  611. }
  612. var getRecordType = function() {
  613. return $("form[name='SearchIndexes'] input[type='radio'][name='index']:checked").val();
  614. }
  615.  
  616. var buildResources = function() {
  617. resources = {
  618.  
  619. baseStyle: `
  620. <style type="text/css">
  621. body
  622. {
  623. min-height: 1200px;
  624. background-color: #EAEAEA;
  625. }
  626. /* widen the page */
  627. body > table[width="800"]
  628. {
  629. width: 960px !important;
  630. }
  631. /* widen header */
  632. table[width="780"][height="80"].banner
  633. {
  634. width: 100% !important;
  635. }
  636.  
  637. /* widen content area */
  638. body > table[width="800"] td[width="600"],
  639. body > table[width="800"] table[width="600"]
  640. {
  641. width: 760px !important;
  642. }
  643.  
  644. form[name="SearchIndexes"]
  645. {
  646. position: relative !important;
  647. }
  648. .groish_ButtonContainer
  649. {
  650. padding-bottom: 10px;
  651. }
  652. .groish_ButtonContainer input[type='submit'],
  653. .groish_ButtonContainer input[type='button']
  654. {
  655. margin-right: 20px;
  656. min-width: 100px;
  657. font-size: 13px;
  658. padding: 6px 10px;
  659. background-color: #15377E;
  660. border-width: 0px;
  661. }
  662. .groish_ButtonContainer input[type='submit']
  663. {
  664. margin-right: 0px;
  665. }
  666. #groish_ResultsCopier,
  667. #groish_ViewSwitcher
  668. {
  669. display:inline-block;
  670. position: absolute;
  671. bottom: 0px;
  672. color: #0076C0;
  673. font-weight: bold;
  674. cursor: pointer;
  675. }
  676. #groish_ResultsCopier
  677. {
  678. right: 120px;
  679. }
  680. #groish_ViewSwitcher
  681. {
  682. right: 10px;
  683. }
  684. div[results-view] td[sort-fields]:hover
  685. {
  686. cursor: pointer;
  687. }
  688.  
  689. .groish_Message
  690. {
  691. position: absolute;
  692. bottom: -30px;
  693. left: 5px;
  694. }
  695.  
  696. </style>
  697. `,
  698.  
  699. view_EW_Birth_Table: `
  700. <style type="text/css">
  701. div[results-view='EW_Birth-Table'] td
  702. {
  703. padding: 5px 3px;
  704. font-size: 75%;
  705. color: #222;
  706. vertical-align: top;
  707. }
  708. div[results-view='EW_Birth-Table'] thead td
  709. {
  710. font-weight: bold;
  711. }
  712. div[results-view='EW_Birth-Table'] tbody tr:nth-child(4n+1),
  713. div[results-view='EW_Birth-Table'] tbody tr:nth-child(4n+2)
  714. {
  715. background-color: #CCE0FF;
  716. }
  717. div[results-view='EW_Birth-Table'] tr.rec-actions a
  718. {
  719. padding: 0px 5px;
  720. font-size: 90%;
  721. color: #15377E;
  722. text-decoration: none;
  723. }
  724. </style>
  725. <div results-view='EW_Birth-Table' style='display: none; margin-bottom: 25px' default-sort-fields='year,quarter'>
  726. <table style='width: 100%; border-collapse: collapse'>
  727. <thead>
  728. <tr>
  729. <td style='width: 12%' sort-fields='year,quarter'>Date</td>
  730. <td style='width: 30%' sort-fields='forenames,surname'>Name</td>
  731. <td style='width: 15%' sort-fields='mother'>Mother</td>
  732. <td style='width: 25%' sort-fields='district'>District</td>
  733. <td style='width: 6%' sort-fields='volume,district'>Vol</td>
  734. <td style='width: 6%' sort-fields='page,volume'>Page</td>
  735. <td style='width: 6%' sort-fields='copy,volume'>Copy</td>
  736. </tr>
  737. </thead>
  738. <tbody>
  739. {{#each items}}
  740. <tr class='rec'>
  741. <td>{{year}} Q{{quarter}}</td>
  742. <td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span>{{#if noForenames}} ({{gender}}){{/if}}</td>
  743. <td>{{mother}}</td>
  744. <td>{{district}}</td>
  745. <td>{{volume}}</td>
  746. <td>{{page}}</td>
  747. <td>{{copy}}</td>
  748. </tr>
  749. <tr class='rec-actions' style='display: none'>
  750. <td colspan='7' style='text-align: right'>
  751. {{#actions}}
  752. <a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
  753. {{/actions}}
  754. </td>
  755. </tr>
  756. {{/each}}
  757. </tbody>
  758. </table>
  759. {{#if failures}}
  760. <p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
  761. <!--
  762. {{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
  763. -->
  764. {{/if}}
  765. <p class='main_text groish_Message'></p>
  766. </div>`,
  767. view_EW_Birth_Delimited: `
  768. <style type="text/css">
  769. div[results-view='EW_Birth-Delimited'] td
  770. {
  771. padding: 5px 3px;
  772. font-size: 75%;
  773. color: #222;
  774. vertical-align: top;
  775. }
  776. div[results-view='EW_Birth-Delimited'] thead td
  777. {
  778. font-weight: bold;
  779. }
  780. div[results-view='EW_Birth-Delimited'] tbody tr:nth-child(odd)
  781. {
  782. background-color: #CCE0FF;
  783. }
  784.  
  785. </style>
  786. <div results-view='EW_Birth-Delimited' style='display: none; margin-bottom: 25px' default-sort-fields='year,quarter'>
  787. <table style='width: 100%; border-collapse: collapse'>
  788. <thead>
  789. <tr>
  790. <td style='width: 100%' sort-fields='year,quarter'>Births</td>
  791. </tr>
  792. </thead>
  793. <tbody>
  794. {{#each items}}
  795. <tr class='rec'>
  796. <td>
  797. {{year}} Q{{quarter}} Birth:
  798. {{forenames}} {{surname}}{{#if noForenames}} ({{gender}}){{/if}}
  799. (mmn: {{mother}});
  800. {{district}};{{#if volume}} Vol {{volume}};{{/if}}{{#if page}} Page {{page}};{{/if}}{{#if copy}} Copy {{copy}};{{/if}}
  801. </td>
  802. </tr>
  803. {{/each}}
  804. </tbody>
  805. </table>
  806. {{#if failures}}
  807. <p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
  808. <!--
  809. {{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
  810. -->
  811. {{/if}}
  812. <p class='main_text groish_Message'></p>
  813. </div>`,
  814.  
  815. view_EW_Death_Table: `
  816. <style type="text/css">
  817. div[results-view='EW_Death-Table'] td
  818. {
  819. padding: 5px 3px;
  820. font-size: 75%;
  821. color: #222;
  822. vertical-align: top;
  823. }
  824. div[results-view='EW_Death-Table'] thead td
  825. {
  826. font-weight: bold;
  827. }
  828. div[results-view='EW_Death-Table'] tbody tr:nth-child(4n+1),
  829. div[results-view='EW_Death-Table'] tbody tr:nth-child(4n+2)
  830. {
  831. background-color: #CCE0FF;
  832. }
  833. div[results-view='EW_Death-Table'] tr.rec-actions a
  834. {
  835. padding: 0px 5px;
  836. font-size: 90%;
  837. color: #15377E;
  838. text-decoration: none;
  839. }
  840. </style>
  841. <div results-view='EW_Death-Table' style='display: none; margin-bottom: 25px' default-sort-fields='year,quarter'>
  842. <table style='width: 100%; border-collapse: collapse'>
  843. <thead>
  844. <tr>
  845. <td style='width: 12%' sort-fields='year,quarter'>Date</td>
  846. <td style='width: 26%' sort-fields='forenames,surname'>Name</td>
  847. <td style='width: 8%' sort-fields='age'>Age{{#if ageCautionThreshold}}*{{/if}}</td>
  848. <td style='width: 8%' sort-fields='birth'>Birth</td>
  849. <td style='width: 28%' sort-fields='district'>District</td>
  850. {{#if (and dataFormat1984 dataFormat1993)}}
  851. <td style='width: 6%' >Vl/Rg</td>
  852. <td style='width: 6%' >Pg/Ey</td>
  853. {{else if dataFormat1993}}
  854. <td style='width: 6%' sort-fields='reg,district'>Reg</td>
  855. <td style='width: 6%' sort-fields='entry,volume'>Entry</td>
  856. {{else}}
  857. <td style='width: 6%' sort-fields='volume,district'>Vol</td>
  858. <td style='width: 6%' sort-fields='page,volume'>Page</td>
  859. {{/if}}
  860. <td style='width: 6%' sort-fields='copy,volume'>Copy</td>
  861. </tr>
  862. </thead>
  863. <tbody>
  864. {{#each items}}
  865. <tr class='rec'>
  866. <td>{{year}}{{#if quarter}} Q{{quarter}}{{/if}}</td>
  867. <td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span>{{#if noForenames}} ({{gender}}){{/if}}</td>
  868. <td>{{age}}</td>
  869. <td>{{birth}}
  870. <td>{{district}}</td>
  871. <td>{{#if volume}}{{volume}}{{else if reg}}{{reg}}{{/if}}</td>
  872. <td>{{#if page}}{{page}}{{else if entry}}{{entry}}{{/if}}</td>
  873. <td>{{copy}}</td>
  874. </tr>
  875. <tr class='rec-actions' style='display: none'>
  876. <td colspan='8' style='text-align: right'>
  877. {{#actions}}
  878. <a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
  879. {{/actions}}
  880. </td>
  881. </tr>
  882. {{/each}}
  883. </tbody>
  884. </table>
  885. {{#if failures}}
  886. <p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
  887. <!--
  888. {{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
  889. -->
  890. {{/if}}
  891. <p class='main_text groish_Message'></p>
  892. </div>`,
  893.  
  894. view_EW_Death_Delimited: `
  895. <style type="text/css">
  896. div[results-view='EW_Death-Delimited'] td
  897. {
  898. padding: 5px 3px;
  899. font-size: 75%;
  900. color: #222;
  901. vertical-align: top;
  902. }
  903. div[results-view='EW_Death-Delimited'] thead td
  904. {
  905. font-weight: bold;
  906. }
  907. div[results-view='EW_Death-Delimited'] tbody tr:nth-child(odd)
  908. {
  909. background-color: #CCE0FF;
  910. }
  911. </style>
  912. <div results-view='EW_Death-Delimited' style='display: none; margin-bottom: 25px' default-sort-fields='year,quarter'>
  913. <table style='width: 100%; border-collapse: collapse'>
  914. <thead>
  915. <tr>
  916. <td style='width: 100%' sort-fields='year,quarter'>Deaths</td>
  917. </tr>
  918. </thead>
  919. <tbody>
  920. {{#each items}}
  921. <tr class='rec'>
  922. <td>
  923. {{year}}{{#if quarter}} Q{{quarter}}{{/if}} Death:
  924. {{forenames}} {{surname}}{{#if noForenames}} ({{gender}}){{/if}};
  925. {{#if yob}}
  926. Born {{yob}} (age {{age}});
  927. {{else}}
  928. Age {{age}} (b{{birth}});
  929. {{/if}}
  930. {{district}};{{#if volume}} Vol {{volume}};{{/if}}{{#if page}} Page {{page}};{{/if}}{{#if reg}} Reg {{reg}};{{/if}}{{#if entry}} Entry {{entry}};{{/if}}{{#if copy}} Copy {{copy}};{{/if}}
  931. </td>
  932. </tr>
  933. {{/each}}
  934. </tbody>
  935. </table>
  936. {{#if failures}}
  937. <p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
  938. <!--
  939. {{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
  940. -->
  941. {{/if}}
  942. <p class='main_text groish_Message'></p>
  943. </div>`
  944.  
  945. };
  946.  
  947. // Add custom views
  948. // NB: Although GreaseMonkey has replaced the GM_* functions with functions on the GM object
  949. // both ViolentMonkey and TamperMonkey still support the GM_* functions so that's being used.
  950.  
  951. // Custom views are defined as GM values (one value per view). The value name must begin with
  952. // either view_EW_Birth or view_EW_Death. The view may contain CSS and HTML and the views are
  953. // Handlebars.js templates (see default views above for examples).
  954. //console.log("adding custom views");
  955. if (typeof GM_listValues === "function" && typeof GM_getValue === "function") {
  956. let valueKeys = GM_listValues();
  957. for(let i = 0; i < valueKeys.length; i++) {
  958. let valueKey = valueKeys[i];
  959. //console.log("value key: %", valueKey);
  960. if (valueKey && valueKey.length > 13 && (valueKey.startsWith("view_EW_Birth") || valueKey.startsWith("view_EW_Death"))) {
  961. // Check the key isn't already in use
  962. if (!resources.hasOwnProperty(valueKey)) {
  963. let viewContent = GM_getValue(valueKey, null);
  964. if (viewContent) {
  965. //console.log("adding view: %s", valueKey);
  966. resources[valueKey] = viewContent;
  967. }
  968. }
  969. }
  970. }
  971. }
  972. }
  973.  
  974. //Get the ball rolling...
  975. main();
  976. });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址