Textarea Backup with expiry

Retains text entered into textareas and contentEditables, and expires after certain time span.

Verzia zo dňa 10.09.2014. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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

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

/*

version history

2.4 on 08/15/2013:
- Support Google Chrome.

2.3 on 07/20/2013:
- Support dynamically created textareas. To restore, such textareas need to be created first and then use the script command.

2.2.1 on 01/06/2013:
- Remove restriction for textarea under a form.

2.2 on 01/03/2013:
- Add support for elements with "contentEditable" attribute.

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 element 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;

// 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 backed up elements in the page
var targets = [];
// element_id: whether this element is prompted for restoration
var prompted = {};

// CSS selector for backup-able elements
var target_selector = 'textarea, *[contentEditable]';

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

var get_element_key = function(element)
{
	// Greasemonkey key for the backup
	// take URI into consideration
	return element.baseURI + ';' + get_element_id(element);
};

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 get_element_value = function(element)
{
	if (element.nodeName == 'TEXTAREA')
		return element.value;
	else
		return element.innerHTML;
};

var set_element_value = function(element, value)
{
	if (element.nodeName == 'TEXTAREA')
		element.value = value;
	else
		element.innerHTML = value;
};

var commit_backup = function(element)
{
	var element_value = get_element_value(element);

	// backup if value is not empty
	if (!/^\s*$/.test(element_value))
	{
		var bak_payload = append_timestamp(element_value);
		GM_setValue(get_element_key(element), bak_payload);
	}
};

var confirm_restore;

var get_backup_content = function(element)
{
	// 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_element_key(element));
	if (!bak_payload)
		return false;

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

var restore_backup = function(elements, 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(elements[index]);
		if (bak_content !== false)
        	{
			confirm_restore(elements, index, bak_content);
        	}
	}
	else
	{
		for (var i = 0; i < elements.length; ++i)
		{
			var element = elements[i];
			set_element_value(element, get_backup_content(element));
		}
	}
};

confirm_restore = function(elements, index, bak_content)
{
	var element = elements[index];
	
	element.scrollIntoView(false);

	// flash the element
	
	var ori_border = element.style.border;
	var new_border = '2px solid red';
	var toggle = true;
	var flashed = flash_count;
	var interval_id;

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

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

			var msg = "[Textarea Backup] Backup exists for this element, proceed to overwrite with this backup?\n\n";
			msg += bak_content.length > 750 ?
				   bak_content.substr(0, 500) + "\n..." :
				   bak_content;

			if (confirm(msg))
				set_element_value(element, bak_content);

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

	interval_id = setInterval(toggle_border, flash_frequency);
};

var on_focus = function(event)
{
	var element = event.target;
	var element_id = get_element_id(element);

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

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

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

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

var init_backup = function(element)
{
	prompted[get_element_id(element)] = false;

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

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

var restore_all = function()
{
	// restore all targets and set prompted status
	
	for (var i = 0; i < targets.length; ++i)
	{
		var target = targets[i];
		var target_id = get_element_id(target);

		if (!prompted[target_id])
			prompted[target_id] = true;
	
		restore_backup(targets, i);
	}
};

var backup_dynamic = function(evt)
{
    if (evt.target.querySelectorAll == undefined)
        return;
    
	var new_textareas = evt.target.querySelectorAll(target_selector);
    for (var i = 0; i < new_textareas.length; ++i)
	{
        var new_textarea = new_textareas.item(i);
		targets.push(new_textarea);
		init_backup(new_textarea);
	}
};

// 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 stored_bak_index = 0; stored_bak_index < stored_bak.length; ++stored_bak_index)
	{
		var bak_payload = GM_getValue(stored_bak[stored_bak_index]);
		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[stored_bak_index]);
	}
}

var query_result = document.querySelectorAll(target_selector);
for (var query_result_index = 0; query_result_index < query_result.length; ++query_result_index)
{
    var query_item = query_result.item(query_result_index);
	targets.push(query_item);
	init_backup(query_item);
}

if (targets.length > 0)
{
	// save buffer in interval fashion
	if (timely_backup_interval > 0)
	{
		var backup_all = function()
		{
			for (var i = 0; i < targets.length; ++i)
			{
				var target = targets[i];
				if (prompted[get_element_id(target)])
					commit_backup(target);
			}
		};
	
		setInterval(backup_all, timely_backup_interval);
	}
}

document.addEventListener('DOMNodeInserted', backup_dynamic);

GM_registerMenuCommand('Restore all textareas in this page', restore_all);