Last active
April 3, 2018 08:24
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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