Skip to content

Instantly share code, notes, and snippets.

@franzalex
Last active April 3, 2018 08:24
Show Gist options
  • Save franzalex/6bea78d9809ecb43886ad272cabff0ac to your computer and use it in GitHub Desktop.
Save franzalex/6bea78d9809ecb43886ad272cabff0ac to your computer and use it in GitHub Desktop.
Creates a backup of input and textArea fields for restoration after data loss. It offers similar functionality as the Lazarus Firefox extension which is no longer supported.
// ==UserScript==
// @author Crend King
// @contributor Franz Alex Gaisie-Essilfie
// @version 2.4.20170620
// @name Textarea Backup with expiry Fix
// @namespace http://users.soe.ucsc.edu/~kjin
// @description Fix @grant https://greasyfork.org/zh-CN/forum/discussion/8161
// @description Retains text entered into textareas and contentEditables, and expires after certain time span.
// @include http://*
// @include https://*
// @updateURL https://gist.github.com/franzalex/6bea78d9809ecb43886ad272cabff0ac/raw/TextArea_Backup_with_Expiry_Fix.user.js
// @downloadURL https://gist.github.com/franzalex/6bea78d9809ecb43886ad272cabff0ac/raw/TextArea_Backup_with_Expiry_Fix.user.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_registerMenuCommand
// ==/UserScript==
// this script was originally based on http://userscripts.org/scripts/review/7671
//
// Subsequent updates based on https://greasyfork.org/en/scripts/19509
// to support all input fields by Franz Alex Gaisie-Essilfie.
/*
version history
2.4.20170620
- Added general support for <input/> fields
- Added context menu item for conveninetly accessing the restore menu item.
2.4.20160508
- Fix @grant https://greasyfork.org/zh-CN/forum/discussion/8161
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 = 60000;
// keep backup even form is submitted
// make sure expiration is enabled or backup will never be deleted
var keep_after_submission = true;
// set true to display a confirmation window for restoration
// otherwise restore unconditionally
var confirm_overwrite = false;
// 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], input';
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.id || element.name || '';
};
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 if (element.nodeName == 'INPUT')
return element.value;
else
return element.innerHTML;
};
var set_element_value = function(element, value)
{
if (element.nodeName == 'TEXTAREA')
element.value = value;
else if (element.nodeName == 'INPUT')
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))
{
console.log("backing up field: " + get_element_id(element));
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];
var bak_content = get_backup_content(element);
if (bak_content !== false)
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]\nBackup 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 restore_field = function(aEvent)
{
// restore field with specified id andd set prompted status
var id = aEvent.target.getAttribute("nodeId");
for (var i = 0; i < targets.length; ++i)
{
if (get_element_id(targets[i]) == id)
if (!prompted[id])
prompted[id] = true;
console.log("restoring field: " + id);
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);
var observer = new MutationObserver(backup_dynamic);
observer.observe(document, { childList: true });
GM_registerMenuCommand('Restore all textareas in this page', restore_all);
if (!("contextMenu" in document.documentElement &&
"HTMLMenuItemElement" in window)) return;
// 'extension method' to allow `isInList(params)` for strings
if (!String.prototype.isInList) {
String.prototype.isInList = function() {
let value = this.valueOf();
for (let i = 0, l = arguments.length; i < l; i += 1) {
if (arguments[i] === value) return true;
}
return false;
}
}
var body = document.body;
body.addEventListener("contextmenu", initMenu, false);
var menu = body.appendChild(document.createElement("menu"));
menu.outerHTML = '<menu id="userscript-restore-textfields" type="context">\
<menuitem label="Restore all fields"\
icon="data:image/png;base64,\
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACAklEQVQ4jWNgoBZQit8UycDQwAIXCF3F\
JhO5yl4lfkuyQtQmf4nQeaJ4DXCsu/zOKP/QZgaG/4ya6TuznJuvf42a9eF/woLv/+Pmff3v0/Xgn0He\
4aUMDg0cmLpDVzG7td75Gz79w3+TouO3Q6e+/R8z5+v/4Mmv/7u03Prp0X7nX9Ssz/+jZ3/+b1J8/ByK\
SxkYGBhE/JZLeXU/+R8+8zMcW1Sef6oQvdaCgeE/I4PbIm7tzH29fhNe/A+Z9uG/VsbuBhQDpMNXWPj2\
v/ofNPXj/6CpH/8HTvnwXy/30F4GBgYmZHU6mfvmBE398N+66tJnFDmFmE2RvpPe/UfGXv2v/mtn7luA\
bIB44CIxp5Z7/906n/yXCV2lC5dQSdneYNd4579F9eU/puUX3hoVnr6hl3t0r0rarskMDP8ZkQ0xrbj4\
yaPn9X/ZiLV+SGEwl5cvdI4QumJswKT8whenjmf/pSNWeRNSiwEkQldpWdZc/29RffW/eOgqRdJ0e05i\
10rff8Wq9sZ/nexDj0jSKxWxxlA9fc8tk7Lz/w2LTv2Xj1vviVeDYtyGEJWkLXNUknfsUk/d/VI39/B/\
g8IT/3XzjvxXittcj9+60FVsaik7/mlnHfgPw1qZ+/4rJ21/LRe7xpcoJyvEb1yuFLfpgULs+rMKsRvnS\
seuc2JAS1RUAwCko+4cEqjxtgAAAABJRU5ErkJggg==">\
</menuitem>\
</menu>';
var textMenu = body.appendChild(document.createElement("menu"));
textMenu.outerHTML = '<menu id="userscript-restore-thisfield" type="context">\
<menuitem label="Restore this field"\
icon="data:image/png;base64,\
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAByUlEQVQ4jWNgwAIC4gpzAuOKZvhGZmtj\
k0cB/xkYGCdHygdPCZeNmBQmG5mdnbsnOa/mTXh87r3CipZnhfGhk6eEy0ZMCZeNmBwha4LVgBPTw/9f\
XBD9/8KC6P+L1+76f+TMVTju65sAlYv6v6LQ+CJWVxye4P//1HT//yen+f+vqa3/39DcBcfd1ekQuel+\
/5fl6p/DawAy3jo98//qGWVIuPT/pOqE+yKSSpjeONjv9//EFF8UvGX33v9///3DwEeuPPrnHpISh2LA\
gV7f/0cneKPgjTt3///x+y8Gfvjq0//tp+/99wpPSYQbsL/b+//hXg8UvG7rjv+ff/zBwN9+/f3/8uP3\
/7vP3PkDN2Bfl9f/g11uKHjVxq3/3339jRPvO3f/H9yAPe2e//e1uaDg5Ws3/n/16RdOvPvMPYQBu9s8\
/u9pdkLBi1au+//0/U+cePuJ2wgDdjW7/d9Vb4+C5y9d8//Bmx848aajNxEG7Ghy/b+9xhYFz16w4v/t\
l99x4rUHr/2DJ+UdDS7/t1ZYo+Dpc5f+v/bsG068ct8VuAuYFqboHJmXpH4BGU+auegrsQZgBXzi0hbSK\
oYhuLCkkn4QAwMDAwA6Lzbb4i0JdwAAAABJRU5ErkJggg==">\
</menuitem>\
</menu>';
document.querySelector("#userscript-restore-textfields menuitem")
.addEventListener("click", restore_all, false);
document.querySelector("#userscript-restore-thisfield menuitem")
.addEventListener("click", restore_field, false);
function initMenu(aEvent) {
// Executed when user right click on web page body
// aEvent.target is the element you right click on
var node = aEvent.target;
var item = document.querySelector("#userscript-restore-textfields menuitem");
if (node.localName.toLowerCase().isInList("input", "textarea") &&
get_element_id(node) != "")
{
body.setAttribute("contextmenu", "userscript-restore-thisfield");
document.querySelector("#userscript-restore-thisfield menuitem")
.setAttribute("nodeId", get_element_id(node));
}
else if (node.localName != "")
{
body.setAttribute("contextmenu", "userscript-restore-textfields");
}
else
{
body.removeAttribute("contextmenu");
document.querySelector("#userscript-restore-thisfield menuitem")
.removeAttribute("nodeId");
document.querySelector("#userscript-restore-textfields menuitem")
.removeAttribute("nodeId");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment