GRO Index Search Helper

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

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

// ==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.5
// @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=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 class='main_text' style='padding: 5px 3px; font-weight: bold; width: 12%'>Date</td>
				<td>Name</td>
				<td>Mother</td>
				<td style='max-width: 30%'>District</td>
				<td>Vol</td>
				<td>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></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>
</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>Name</td>
				<td>Age{{#if ageCautionThreshold}}*{{/if}}</td>
				<td>Birth</td>
				<td style='max-width: 30%'>District</td>
				<td>Vol</td>
				<td>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></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 ageCautionThreshold}}
		<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>
	{{/if}}
</div>
*************RESOURCE_END*************

======================INLINE_RESOURCE_END======================
*/

this.$ = this.jQuery = jQuery.noConflict(true);

$(function()
{
	
	var initialiseSearchForm = function()
	{
		// Hide the reset button
		$("form[name='SearchIndexes'] input[type='submit'][value='Reset']").hide();

		// Hide superfluous text
		$("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(); 

		// Add gender and year navigation buttons, and style them
		var searchButton = $("form[name='SearchIndexes'] input[type='submit'][value='Search']")

		$("<input class='formButton' id='groish_BtnGenderToggle' type='button' value='Gender' />").insertBefore($(searchButton));
		$("<input class='formButton' id='groish_BtnYearsPrev' type='button' value='&lt; Years' />").insertBefore($(searchButton));
		$("<input class='formButton' id='groish_BtnYearsNext' type='button' value='Years &gt;' />").insertBefore($(searchButton));

		var buttonContainer = $("form[name='SearchIndexes'] input[type='submit'][value='Search']").closest("td");
		$(buttonContainer).css("padding-bottom", "10px");
		$(buttonContainer).find("input[type='button']").css("margin-right", "20px");
		$(buttonContainer).find("input[type='submit'], input[type='button']").css("min-width", "100px").css("font-size", "13px").css("padding", "4px 10px");

		// Add button event handlers
		$("input#groish_BtnYearsPrev").click(function() { navigateYears(false); });
		$("input#groish_BtnYearsNext").click(function() { navigateYears(true); });
		$("input#groish_BtnGenderToggle").click(function() { toggleGender(); });
		
	}
	
	var initialiseResultViews = function(recordType, resources)
	{
		
		//console.log("start: initialiseResultViews");
		
		// 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
		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 click handlers to results header, to toggle views
			$(".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>"))
									.click(function() { switchResultsView(); });
			//$("form[name='SearchIndexes'] h3:contains('Results:')").click(function() { switchResultsView(); });

			// 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();
			}
		}
		
		//console.log("end: initialiseResultViews");
	}
	
	var switchResultsView = function()
	{
		//console.log("switchResultsView");
	
		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)
	{
		//console.log("start: getResults");
		
		var results = { "ageCautionThreshold": 24, "items": [] };
		
		// Lookup record type - birth or death
		if (recordType !== null && (recordType === "EW_Birth" || recordType === "EW_Death"))
		{
			$("div[results-view='default'] > table > tbody > tr")
				.has("img[src='./graphics/order_certificate_button.gif']")
				.each(function(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);

						if (namesArr !== null && refArr !== null)
						{
							var forenames 	= toTitleCase(namesArr[2]).trim();
							var surname 	= toTitleCase(namesArr[1]).trim();
							var year 		= parseInt(refArr[1], 10);
							var quarter 	= parseInt(refArr[2], 10);
							var district 	= toTitleCase(refArr[3]).trim();
							var volume		= refArr[4].toLowerCase();
							var page		= refArr[5];

							//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);
							
							var record =
								{
									"forenames": 	forenames,
									"surname": 		surname,
									"age": 			age,
									"ageCaution": 	(age != null && age > 0 && age <= results.ageCautionThreshold),
									"birth": 		(age != null ? year - age : null),
									"mother": 		mother,
									"year": 		year,
									"quarter": 		quarter,
									"district": 	district,
									"volume": 		volume,
									"page": 		page,
									"actions": 		actions
								};

							results.items.push(record);
							//console.log(record);
						}
						else
						{
							//console.log("Failed to read record: %d", index);
						}
					});
		}

		// 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;
			});
		}
		
		//console.log("end: getResults");

		return results;
	}


	var toTitleCase = function(str)
	{
		return str.replace(/([^\W_]+[^\s-]*) */g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
	}

	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 navigateYears = function(forward)
	{
		// 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(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 step = (curRange * 2) + 1;
				var newYear = (forward ? curYear+step : curYear-step);
				newYear = Math.min(Math.max(newYear, minYear), maxYear);

				// Update the year and submit the search
				$("form[name='SearchIndexes'] select#Year").val(newYear);
				$("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
			}

			//console.log("Current year: %d +-%d (%d-%d), New year: %d (%d-%d)", curYear, curRange, curYear-curRange, curYear+curRange, newYear, newYear-curRange, newYear+curRange);
		}
	}
	
	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...
	var resources = getInlineResources();
	var recordType = getRecordType();
	
	initialiseSearchForm();
	initialiseResultViews(recordType, resources);
});