// ==UserScript==
// @name GRO Index Search Helper
// @description Adds additional functionality to the UK General Register Office (GRO) BMD index search
// @namespace cuffie81.scripts
// @include https://www.gro.gov.uk/gro/content/certificates/indexes_search.asp
// @version 1.6
// @grant none
// @require https://code.jquery.com/jquery-2.2.4.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js
// ==/UserScript==
/*
======================INLINE_RESOURCE_BEGIN======================
***********RESOURCE_START=CSS*************
<style type="text/css">
body
{
min-height: 1200px;
}
.groish_ButtonContainer
{
padding-bottom: 10px;
}
.groish_ButtonContainer input[type='submit'],
.groish_ButtonContainer input[type='button']
{
margin-right: 20px;
min-width: 100px;
font-size: 13px;
padding: 4px 10px;
}
.groish_ButtonContainer input[type='submit']
{
margin-right: 0px;
}
#groish_ResultsSelector,
#groish_ViewSwitcher
{
display:inline-block;
position: absolute;
bottom: 0px;
color: #993333;
font-weight: bold;
cursor: pointer;
}
#groish_ResultsSelector
{
right: 120px;
}
#groish_ViewSwitcher
{
right: 10px;
}
</style>
*************RESOURCE_END*************
***********RESOURCE_START=Template-EW_Birth*************
<style type="text/css">
div[results-view='EW_Birth'] td
{
padding: 5px 3px;
font-size: 75%;
color: #663333;
vertical-align: top;
}
div[results-view='EW_Birth'] thead td
{
font-weight: bold;
}
div[results-view='EW_Birth'] tbody tr:nth-child(4n+1),
div[results-view='EW_Birth'] tbody tr:nth-child(4n+2)
{
background-color: #F9E8A5;
}
div[results-view='EW_Birth'] tr.rec-actions a
{
padding: 0px 5px;
font-size: 90%;
color: #663333;
text-decoration: none;
}
</style>
<div results-view='EW_Birth' style='display: none; margin-bottom: 25px'>
<table style='width: 100%; border-collapse: collapse'>
<thead>
<tr>
<td style='width: 12%; padding: 5px 3px; font-weight: bold;'>Date</td>
<td style='width: 30%'>Name</td>
<td style='width: 15%'>Mother</td>
<td style='width: 27%'>District</td>
<td style='width: 8%'>Vol</td>
<td style='width: 8%'>Page</td>
</tr>
</thead>
<tbody>
{{#each items}}
<tr class='rec'>
<td>{{year}} Q{{quarter}}</td>
<td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span>{{#if noForenames}} ({{gender}}){{/if}}</td>
<td>{{mother}}</td>
<td>{{district}}</td>
<td>{{volume}}</td>
<td>{{page}}</td>
</tr>
<tr class='rec-actions' style='display: none'>
<td colspan='6' style='text-align: right'>
{{#actions}}
<a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
{{/actions}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{#if failures}}
<p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
<!--
{{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
-->
{{/if}}
</div>
*************RESOURCE_END*************
***********RESOURCE_START=Template-EW_Death*************
<style type="text/css">
div[results-view='EW_Death'] td
{
padding: 5px 3px;
font-size: 75%;
color: #663333;
vertical-align: top;
}
div[results-view='EW_Death'] thead td
{
font-weight: bold;
}
div[results-view='EW_Death'] tbody tr:nth-child(4n+1),
div[results-view='EW_Death'] tbody tr:nth-child(4n+2)
{
background-color: #F9E8A5;
}
div[results-view='EW_Death'] tr.rec-actions a
{
padding: 0px 5px;
font-size: 90%;
color: #663333;
text-decoration: none;
}
</style>
<div results-view='EW_Death' style='display: none; margin-bottom: 25px'>
<table style='width: 100%; border-collapse: collapse'>
<thead>
<tr>
<td style='width: 12%'>Date</td>
<td style='width: 26%'>Name</td>
<td style='width: 8%'>Age{{#if ageCautionThreshold}}*{{/if}}</td>
<td style='width: 8%'>Birth</td>
<td style='width: 30%'>District</td>
<td style='width: 8%'>Vol</td>
<td style='width: 8%'>Page</td>
</tr>
</thead>
<tbody>
{{#each items}}
<tr class='rec'>
<td>{{year}} Q{{quarter}}</td>
<td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span>{{#if noForenames}} ({{gender}}){{/if}}</td>
<td>{{age}}{{#if ageCaution}}*{{/if}}</td>
<td>{{birth}}
<td>{{district}}</td>
<td>{{volume}}</td>
<td>{{page}}</td>
</tr>
<tr class='rec-actions' style='display: none'>
<td colspan='7' style='text-align: right'>
{{#actions}}
<a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
{{/actions}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{#if failures}}
<p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
<!--
{{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
-->
{{/if}}
<p class='main_text'>
* Age is presumed to be years but <i>may</i> be months.
{{#if ageCautionThreshold}}An age below {{ageCautionThreshold}} <i>may</i> be a child, treat with caution.{{/if}}
An age of zero <i>may</i> have be used when a child was aged less than 12 months.
</p>
</div>
*************RESOURCE_END*************
======================INLINE_RESOURCE_END======================
*/
this.$ = this.jQuery = jQuery.noConflict(true);
$(function()
{
var resources, recordType;
var main = function()
{
resources = getInlineResources();
recordType = getRecordType();
//console.log("resources:\r\n%s", JSON.stringify(resources));
// Load the general css
var cssBlock = resources["CSS"].toString();
$("body").append($(cssBlock));
initialiseSearchForm();
initialiseResultViews(recordType, resources);
// Scroll down to the form. Do this last as we may add/remove/chnage elements in the previous calls.
$("h1:contains('Search the GRO Online Index')")[0].scrollIntoView();
// Wire up accesskeys to clicks, to avoid having to use the full accesskey combo (eg ALT+SHFT+#)
$(document).on("keypress", function(e)
{
if (!document.activeElement || document.activeElement.tagName.toLowerCase() !== "input")
{
var char = String.fromCharCode(e.which);
//console.log("keypress: %s", char);
if ($("*[id^='groish'][accesskey='" + char + "']").length)
$("*[id^='groish'][accesskey='" + char + "']").click();
else if (char == "{")
adjustSearchYear(-10);
else if (char == "}")
adjustSearchYear(10);
else if (char == "?")
$("form[name='SearchIndexes'] input[type='submit']").click();
else if (char == '@')
switchRecordType();
}
});
}
var initialiseSearchForm = function()
{
// Hide superfluous spacing, text and buttons
$("body > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2)").hide();
$("h1:contains('Search the GRO Online Index')").closest("tr").next().hide();
$("strong:contains('Which index would you like to search?')").closest("tr").hide();
$("table[summary*='contains the search form fields'] > tbody > tr:nth-of-type(2)").hide();
$("table[summary*='contains the search form fields'] > tbody > tr:nth-of-type(3) td.main_text[colspan='5']").parent().hide();
$("form[name='SearchIndexes'] input[type='submit'][value='Reset']").hide();
// Change text
$("form[name='SearchIndexes'] td span.main_text:contains('year(s)')").text("yrs");
$("form[name='SearchIndexes'] td.main_text:contains('First Forename at Death:')").text("Forename 1:");
$("form[name='SearchIndexes'] td.main_text:contains('Second Forename at Death:')").text("Forename 2:");
$("form[name='SearchIndexes'] td.main_text:contains('District of Death:')").text("District:");
$("form[name='SearchIndexes'] td.main_text:contains('First Forename:')").text("Forename 1:");
$("form[name='SearchIndexes'] td.main_text:contains('Second Forename:')").text("Forename 2:");
$("form[name='SearchIndexes'] td.main_text:contains('Maiden Surname:')").text("Mother:");
$("form[name='SearchIndexes'] td.main_text:contains('District of Birth:')").text("District:");
// Add gender and year navigation buttons, and style them
var searchButton = $("form[name='SearchIndexes'] input[type='submit'][value='Search']");
$(searchButton).attr("accesskey", "?");
$(searchButton).parent().find("br").remove();
$("<input type='button' class='formButton' accesskey='#' id='groish_BtnToggleGender' value='Gender' />").insertBefore($(searchButton));
$("<input type='button' class='formButton' accesskey='[' id='groish_BtnYearsPrev' value='< Years' />").insertBefore($(searchButton));
$("<input type='button' class='formButton' accesskey=']' id='groish_BtnYearsNext' value='Years >' />").insertBefore($(searchButton));
var buttonContainer = $("form[name='SearchIndexes'] input[type='submit'][value='Search']").closest("td").addClass("groish_ButtonContainer");
// Add button event handlers
$("input#groish_BtnYearsPrev").click(function() { navigateYears(false); });
$("input#groish_BtnYearsNext").click(function() { navigateYears(true); });
$("input#groish_BtnToggleGender").click(function() { toggleGender(); });
}
var initialiseResultViews = function(recordType, resources)
{
// Move default results table into a view container
var defaultTable = $("form[name='SearchIndexes'] h3:contains('Results:')").closest("table").css("width", "100%").addClass("groish_ResultsTable");
$(defaultTable).before($("<div results-view='default' />"));
var defaultView = $("div[results-view='default']");
$(defaultView).append($("table.groish_ResultsTable"));
// Move header row to before default view
$(defaultView).before($("<div class='groish_ResultsHeader' style='margin: 10px 0px; position: relative' />"));
$(".groish_ResultsHeader").append($("table.groish_ResultsTable h3:contains('Results:')"));
// Move pager row contents to after default view
$(defaultView).after($("table.groish_ResultsTable > tbody > tr:last table:first"));
$("div[results-view='default'] + table").css("width", "100%").addClass("groish_ResultsInfo");
// Add alternate view(s)
if (recordType)
{
var results = getResults(recordType);
//console.log(results);
if (results != null && recordType && results.items != null && results.items.length > 0)
{
// Get template and add alternate view
var template = resources["Template-" + recordType].toString();
var compiledTemplate = Handlebars.compile(template);
var html = compiledTemplate(results);
$(defaultView).after($(html));
// Add event handler to hide/show actions row
// TODO: Make adding view event handlers more dynamic, so they can be specific to the view
$("div[results-view][results-view!='default'] tbody tr.rec").click(function(index)
{
$(this).next("tr.rec-actions:not(:empty)").toggle();
});
// Add view switcher
$(".groish_ResultsHeader").append($("<a href='#' id='groish_ViewSwitcher' class='main_text' accesskey='~'>Switch view</a>"));
$("#groish_ViewSwitcher").on("click", function() { switchResultsView(); return false; });
// Add results selector (if supported)
if (window.getSelection && document.createRange)
{
$(".groish_ResultsHeader").append($("<a href='#' id='groish_ResultsSelector' class='main_text' accesskey='|'>Select results</a>"));
$("#groish_ResultsSelector").on("click", function()
{
var resultsBody = $("div[results-view]:visible tbody")[0];
if (resultsBody)
{
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(resultsBody);
selection.removeAllRanges();
selection.addRange(range);
}
return false;
});
}
// Show the last used view
var viewName = sessionStorage.getItem("groish_view." + recordType);
//console.log("initialising view: %s", viewName);
if (viewName && $("div[results-view='" + viewName + "']:hidden").length == 1)
{
//console.log("setting active view: %s", viewName);
$("div[results-view][results-view!='" + viewName + "']").hide();
$("div[results-view][results-view='" + viewName + "']").show();
}
}
}
}
var switchResultsView = function()
{
var recordType = getRecordType();
var views = $("div[results-view]");
if (views.length > 1)
{
var curIndex = -1;
$(views).each(function(index)
{
if ($(this).css("display") != "none")
curIndex = index;
});
//console.log("current view index: %s", curIndex);
if (curIndex !== -1)
{
var newIndex = ((curIndex == (views.length-1)) ? 0 : curIndex+1);
$(views).hide();
$("div[results-view]:eq(" + newIndex + ")").show();
// Get the name and save it
var viewName = $("div[results-view]:eq(" + newIndex + ")").attr("results-view")
sessionStorage.setItem("groish_view." + recordType, viewName); //save it
//console.log("new view: %s", viewName);
}
}
}
var getResults = function(recordType)
{
var results = { "ageCautionThreshold": 24, "items": [], "failures": [] };
// Lookup record type - birth or death
if (recordType !== null && (recordType === "EW_Birth" || recordType === "EW_Death"))
{
var gender = $("form[name='SearchIndexes'] select#Gender").val();
$("div[results-view='default'] > table > tbody > tr")
.has("img[src='./graphics/order_certificate_button.gif']")
.each(function(index)
{
try
{
//console.log("Parsing record (%d)...", index);
// Get names and reference
var names = $(this).find("td:eq(0)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim();
var ref = $(this).next().find("td:eq(0)").text();
// Clean up reference
ref = ref.replace(/\u00a0/g, " ");
ref = ref.replace(/\s\s+/g, ' ');
ref = ref.replace(/GRO Reference: /g, "");
ref = ref.replace(/M Quarter in/g, "Q1");
ref = ref.replace(/J Quarter in/g, "Q2");
ref = ref.replace(/S Quarter in/g, "Q3");
ref = ref.replace(/D Quarter in/g, "Q4");
var age = 0;
if (recordType === "EW_Death")
{
var ageArr = /^([0-9]{1,3})$/.exec($(this).find("td:eq(1)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim());
if (ageArr)
age = parseInt(ageArr[1], 10);
}
var mother = null;
if (recordType === "EW_Birth")
mother = toTitleCase($(this).find("td:eq(1)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ')).trim();
var actions = [];
var orderCertUrl = $(this).find("a[href^='indexes_order.asp']:eq(0)").prop("href");
var orderPdfUrl = $(this).next().find("a[href^='indexes_order.asp']:eq(0)").prop("href");
if (orderCertUrl) actions.push( {"text": "Order Certificate", "url": orderCertUrl });
if (orderPdfUrl) actions.push( {"text": "Order Research Copy", "title": "PDF", "url": orderPdfUrl });
// Parse forenames, surname, year, quarter, district, vol, page
var namesArr = /([a-z' -]+),([a-z' -]*)/gi.exec(names);
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
//console.log("index: %d, namesArr: %s, refArr: %s", index, namesArr, refArr);
var record =
{
"gender": gender,
"forenames": toTitleCase(namesArr[2]).trim(),
"surname": toTitleCase(namesArr[1]).trim(),
"age": age,
"mother": mother,
"year": parseInt(refArr[1], 10),
"quarter": parseInt(refArr[2], 10),
"district": toTitleCase(refArr[3]).trim(),
"volume": refArr[4].toLowerCase(),
"page": refArr[5],
"actions": actions
};
record.noForenames = (!record.forenames || record.forenames == "-");
record.ageCaution = (age != null && age > 0 && age <= results.ageCautionThreshold);
record.birth = (age != null ? record.year - age : null);
//console.log(record);
results.items.push(record);
}
catch (e)
{
//console.log("Failed to parse record (%d): %s", index, e.message);
results.failures.push({ "index": index, "ex": e });
}
});
}
// Sort records
if (results.items.length > 0)
{
results.items.sort(function(a, b)
{
if (a.year == b.year && a.quarter == b.quarter)
return 0;
else if ((a.year > b.year) || (a.year == b.year && a.quarter > b.quarter))
return 1;
else
return -1;
});
}
return results;
}
var toTitleCase = function(str)
{
return str.replace(/([^\W_]+[^\s-]*) */g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
var switchRecordType = function()
{
var recordTypes = $("form[name='SearchIndexes'] input[type='Radio'][name='index']");
var curIndex = -1;
for (var i = 0; i < recordTypes.length; i++)
{
if ($(recordTypes).eq(i).prop("checked"))
{
curIndex = i;
break;
}
}
//console.log("current record type: %d", curIndex);
if (curIndex >= 0)
{
var nextIndex = (curIndex == (recordTypes.length-1)) ? 0 : curIndex + 1;
if (nextIndex != curIndex)
$(recordTypes).eq(nextIndex).prop("checked", true).click();
//console.log("next record type: %d", nextIndex);
}
}
var toggleGender = function()
{
var curGender = $("form[name='SearchIndexes'] select#Gender").val();
$("form[name='SearchIndexes'] select#Gender").val((curGender === "F" ? "M" : "F"));
$("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
}
var adjustSearchYear = function(step)
{
var adjusted = false;
// Get min and max years
var minYear = parseInt($("form[name='SearchIndexes'] select#Year option:eq(2)").val(), 10);
var maxYear = parseInt($("form[name='SearchIndexes'] select#Year option:last").val(), 10);
//console.log("Year range: %s - %s", minYear, maxYear);
if (!isNaN(step) && !isNaN(minYear) && !isNaN(maxYear))
{
// Read current year and range
var curYear = parseInt($("form[name='SearchIndexes'] select#Year").val(), 10);
var curRange = parseInt($("form[name='SearchIndexes'] select#Range").val(), 10);
if (!isNaN(curYear) && !isNaN(curRange))
{
// Calculate the new year
var newYear = curYear+step;
newYear = Math.min(Math.max(newYear, minYear), maxYear);
if (newYear != curYear)
{
$("form[name='SearchIndexes'] select#Year").val(newYear);
adjusted = true;
}
}
//console.log("Current year: %d +-%d (%d-%d), New year: %d (%d-%d)", curYear, curRange, curYear-curRange, curYear+curRange, newYear, newYear-curRange, newYear+curRange);
}
return adjusted;
}
var navigateYears = function(forward)
{
var curRange = parseInt($("form[name='SearchIndexes'] select#Range").val(), 10);
if (!isNaN(curRange))
{
// Calculate the new year
var step = (curRange * 2) + 1;
if (!forward) step = -step;
if (adjustSearchYear(step))
{
$("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
}
}
}
var getRecordType = function()
{
return $("form[name='SearchIndexes'] input[type='radio'][name='index']:checked").val();
}
// https://gist.github.com/aidanhs/5534196
var getInlineResources = function()
{
var resource = {}, len, match, resourceBlocks, inlineResourcesMatch = (/^=+INLINE_RESOURCE_BEGIN=+$([\s\S]*?)^=+INLINE_RESOURCE_END=+$/m).exec(GM_info.scriptSource);
resourceBlocks = (inlineResourcesMatch && inlineResourcesMatch[1].match(/^\**RESOURCE_START[\s\S]*?^\**RESOURCE_END\**$/mg)) || null;
len = (resourceBlocks && resourceBlocks.length) || 0;
for (var i = 0; i < len; i++)
{
match = (/^\**RESOURCE_START=(.*?)\**$\s*^([\s\S]*)^\**RESOURCE_END\**$/m).exec(resourceBlocks[i]);
resource[match[1]] = match[2];
}
return resource;
}
//Get the ball rolling...
main();
});