Textarea Backup with expiry

By Crend King Last update May 10, 2011 — Installed 8,087 times.

There are 30 previous versions of this script.

// ==UserScript==
// @author        Crend King
// @version       2.1
// @name          Textarea Backup with expiry
// @namespace     http://users.soe.ucsc.edu/~kjin
// @description   Retains text entered into textareas, and expires after certain time span.
// @include       http://*
// @include       https://*
// ==/UserScript==

// this script was originally based on http://userscripts.org/scripts/review/7671

// check latest version at http://userscripts.org/scripts/show/42879

/*

version history

2.1 on 05/09/2011:
- Add user menu command to restore all textarea in the page.

2.0 on 05/06/2011:
- Completely rewrite the script. New script should be faster, stronger and more standard-compliant.
- Fix bugs in previous versions and the original script.

1.0.4 on 04/22/2009:
- Synchronize with the original Textarea Backup script.

1.0.3 on 03/08/2009:
- Add "ask overwrite" option.

1.0.2 on 03/04/2009:
- Add "keep after submission" option.

1.0.1 on 02/22/2009:
- Extract the expiry time stamp codes to stand-alone functions.

1.0 on 02/21/2009:
- Initial version.

*/


///// preference section /////

// backup when textarea loses focus
var blur_backup = true;

// interval for timely backup, in millisecond. 0 disables timely backup
var timely_backup_interval = 0;

// keep backup even form is submitted
// make sure expiration is enabled or backup will never be deleted
var keep_after_submission = false;

// set true to display a confirmation window for restoration
// otherwise restore unconditionally
var confirm_overwrite = true;

// auxiliary variable to compute expiry_timespan
// set all 0 to disable expiration
var expire_after_days = 0;
var expire_after_hours = 0;
var expire_after_minutes = 30;


///// code section /////

// expiry time for a backup, in millisecond
var expiry_timespan = (((expire_after_days * 24) + expire_after_hours) * 60 + expire_after_minutes) * 60000;
// RegExp to check if string is consisted of all spaces
var empty_regexp = /^\s*$/;

// how many times to flash. must be a even number, or the border style will not revert
var flash_count = 6;
// how fast is the flash
var flash_frequency = 100;

// array of all textarea elements in the page
var textareas = [];
// textarea_id: whether this textarea is prompted for restoration
var prompted = {};

var get_ta_id = function(ta)
{
	/*
	return the reference ID of the textarea
	multiple textareas with no name or id will collide
	but textarea without either would be useless
	*/
	return ta.name || ta.id || '';
};

var get_ta_key = function(ta)
{
	// Greasemonkey key for the backup
	// take URI into consideration
	return ta.baseURI + ';' + get_ta_id(ta);
};

var append_timestamp = function(str)
{
	return str + '@' + (new Date()).getTime();
};

var remove_timestamp = function(str)
{
	return str.replace(/@\d+$/, '');
};

var get_timestamp = function(str)
{
	var time_pos = str.lastIndexOf('@');
	return str.substr(time_pos + 1);
};

var commit_backup = function(ta)
{
	// backup if value is not empty
	if (!empty_regexp.test(ta.value))
	{
		var bak_payload = append_timestamp(ta.value);
		GM_setValue(get_ta_key(ta), bak_payload);
	}
};

var flash_textarea = function(ta)
{
	// flash the textarea
	
	var ori_border = ta.style.border;
	var new_border = '2px solid red';
	var toggle = true;
	var flashed = flash_count;
	var interval_id;

	var toggle_border = function()
	{
		ta.style.border = (toggle ? new_border : ori_border);
		toggle = !toggle;

		--flashed;
		if (flashed == 0)
			clearInterval(interval_id);
	};

	interval_id = setInterval(toggle_border, flash_frequency);
};

var confirm_restore = function(tas, index, bak_content)
{
	var ta = tas[index];
	
	ta.scrollIntoView();
	flash_textarea(ta);
	
	var confirmation = function()
	{
		var msg = "[Textarea Backup] Backup exists for this textarea, proceed to overwrite with this backup?\n\n";
		msg += bak_content.length > 750 ?
			   bak_content.substr(0, 500) + "\n..." :
			   bak_content;

		if (confirm(msg))
			ta.value = bak_content;

		if (index + 1 < tas.length)
		{
			// setTimeout is an asynchronized operation
			// need recursion to serialize restoration on textareas
			restore_backup(tas, index + 1);
		}
	};

	// setInterval in flash_textarea is an asynchronized operation
	// need to wait until flashing is finished
	setTimeout(confirmation, (flash_count + 1) * flash_frequency);
};


var get_backup_content = function(ta)
{
	// backup payload is in format of "backup_text@save_time",
	// where save_time is the millisecond from Javascript Date object's getTime()
	var bak_payload = GM_getValue(get_ta_key(ta));
	if (!bak_payload)
		return false;

	var bak_content = remove_timestamp(bak_payload);
	
	// ignore if backup text is identical to current value
	if (bak_content == ta.value)
		return false;
	else
		return bak_content;
};

var restore_backup = function(tas, index)
{
	// check with user before overwriting existing content with backup
	// asynchronized when confirmation is enabled, synchronized otherwise
	if (confirm_overwrite)
	{
		var bak_content = get_backup_content(tas[index]);
		if (bak_content !== false)
			confirm_restore(tas, index, bak_content);
	}
	else
	{
		for (var i = 0; i < tas.length; ++i)
		{
			var ta = tas[i];
			ta.value = get_backup_content(ta);
		}
	}
};

var on_focus = function(event)
{
	var ta = event.target;
	var ta_id = get_ta_id(ta);

	if (!prompted[ta_id])
	{
		// set prompted status disregarding user's choice of overwriting
		prompted[ta_id] = true;

		restore_backup([ta], 0);
	}
};

var on_blur = function(event)
{
	commit_backup(event.target);
};

var on_submit = function(event)
{
	for (var i = 0; i < textareas.length; ++i)
		GM_deleteValue(get_ta_key(textareas[i]));
};

var init_backup = function(ta)
{
	// textarea-specific initialization
	
	prompted[get_ta_id(ta)] = false;

	ta.addEventListener('focus', on_focus, true);

	// save buffer when the textarea loses focus
	if (blur_backup)
		ta.addEventListener('blur', on_blur, true);
	
	// delete buffer when the form is submitted
	if (!keep_after_submission)
		ta.form.addEventListener('submit', on_submit, true);
};

var restore_all = function()
{
	// restore all textareas and set prompted status
	
	for (var i = 0; i < textareas.length; ++i)
	{
		var ta = textareas[i];
		var ta_id = get_ta_id(ta);

		if (!prompted[ta_id])
			prompted[ta_id] = true;
	}

	restore_backup(textareas, 0);
};

// expiration check routine
if (expiry_timespan > 0)
{
	// get all associated backups for this page, and compare timestamp now and then
	var curr_time = new Date().getTime();
	var stored_bak = GM_listValues();

	for (var i = 0; i < stored_bak.length; ++i)
	{
		var bak_payload = GM_getValue(stored_bak[i]);
		var bak_content = remove_timestamp(bak_payload);
		var bak_time = get_timestamp(bak_payload);
		
		if (curr_time - bak_time >= expiry_timespan)
			GM_deleteValue(stored_bak[i]);
	}
}

textareas = document.getElementsByTagName('textarea');
for (var i = 0; i < textareas.length; ++i)
{
	var ta = textareas[i];

	// process textarea only if it is under a form
	if (ta.form)
		init_backup(ta);
}

if (textareas.length > 0)
{
	// save buffer in interval fashion
	if (timely_backup_interval > 0)
	{
		var backup_all = function()
		{
			for (var i = 0; i < textareas.length; ++i)
			{
				var ta = textareas[i];
	
				if (prompted[get_ta_id(ta)])
					commit_backup(ta);
			}
		};
	
		setInterval(backup_all, timely_backup_interval);
	}
	
	GM_registerMenuCommand('Restore all textareas in this page', restore_all);
}