GRO Index Search Helper

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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='&lt; Years' />").insertBefore($(searchButton));
		$("<input type='button' class='formButton' accesskey=']' id='groish_BtnYearsNext' value='Years &gt;' />").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();
});