Textarea Backup with expiry

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

  1. // ==UserScript==
  2. // @author Crend King
  3. // @version 2.4
  4. // @name Textarea Backup with expiry
  5. // @namespace http://users.soe.ucsc.edu/~kjin
  6. // @description Retains text entered into textareas and contentEditables, and expires after certain time span.
  7. // @include http://*
  8. // @include https://*
  9. // @homepage http://userscripts.org/scripts/show/42879
  10. // ==/UserScript==
  11.  
  12. // this script was originally based on http://userscripts.org/scripts/review/7671
  13.  
  14. /*
  15.  
  16. version history
  17.  
  18. 2.4 on 08/15/2013:
  19. - Support Google Chrome.
  20.  
  21. 2.3 on 07/20/2013:
  22. - Support dynamically created textareas. To restore, such textareas need to be created first and then use the script command.
  23.  
  24. 2.2.1 on 01/06/2013:
  25. - Remove restriction for textarea under a form.
  26.  
  27. 2.2 on 01/03/2013:
  28. - Add support for elements with "contentEditable" attribute.
  29.  
  30. 2.1 on 05/09/2011:
  31. - Add user menu command to restore all textarea in the page.
  32.  
  33. 2.0 on 05/06/2011:
  34. - Completely rewrite the script. New script should be faster, stronger and more standard-compliant.
  35. - Fix bugs in previous versions and the original script.
  36.  
  37. 1.0.4 on 04/22/2009:
  38. - Synchronize with the original Textarea Backup script.
  39.  
  40. 1.0.3 on 03/08/2009:
  41. - Add "ask overwrite" option.
  42.  
  43. 1.0.2 on 03/04/2009:
  44. - Add "keep after submission" option.
  45.  
  46. 1.0.1 on 02/22/2009:
  47. - Extract the expiry time stamp codes to stand-alone functions.
  48.  
  49. 1.0 on 02/21/2009:
  50. - Initial version.
  51.  
  52. */
  53.  
  54.  
  55. ///// preference section /////
  56.  
  57. // backup when element loses focus
  58. var blur_backup = true;
  59.  
  60. // interval for timely backup, in millisecond. 0 disables timely backup
  61. var timely_backup_interval = 0;
  62.  
  63. // keep backup even form is submitted
  64. // make sure expiration is enabled or backup will never be deleted
  65. var keep_after_submission = false;
  66.  
  67. // set true to display a confirmation window for restoration
  68. // otherwise restore unconditionally
  69. var confirm_overwrite = true;
  70.  
  71. // auxiliary variable to compute expiry_timespan
  72. // set all 0 to disable expiration
  73. var expire_after_days = 0;
  74. var expire_after_hours = 0;
  75. var expire_after_minutes = 30;
  76.  
  77.  
  78. ///// code section /////
  79.  
  80. // expiry time for a backup, in millisecond
  81. var expiry_timespan = (((expire_after_days * 24) + expire_after_hours) * 60 + expire_after_minutes) * 60000;
  82.  
  83. // how many times to flash. must be a even number, or the border style will not revert
  84. var flash_count = 6;
  85. // how fast is the flash
  86. var flash_frequency = 100;
  87.  
  88. // array of all backed up elements in the page
  89. var targets = [];
  90. // element_id: whether this element is prompted for restoration
  91. var prompted = {};
  92.  
  93. // CSS selector for backup-able elements
  94. var target_selector = 'textarea, *[contentEditable]';
  95.  
  96. var get_element_id = function(element)
  97. {
  98. /*
  99. return the reference ID of the element
  100. multiple elements with no name or id will collide
  101. but element without either would be useless
  102. */
  103. return element.name || element.id || '';
  104. };
  105.  
  106. var get_element_key = function(element)
  107. {
  108. // Greasemonkey key for the backup
  109. // take URI into consideration
  110. return element.baseURI + ';' + get_element_id(element);
  111. };
  112.  
  113. var append_timestamp = function(str)
  114. {
  115. return str + '@' + (new Date()).getTime();
  116. };
  117.  
  118. var remove_timestamp = function(str)
  119. {
  120. return str.replace(/@\d+$/, '');
  121. };
  122.  
  123. var get_timestamp = function(str)
  124. {
  125. var time_pos = str.lastIndexOf('@');
  126. return str.substr(time_pos + 1);
  127. };
  128.  
  129. var get_element_value = function(element)
  130. {
  131. if (element.nodeName == 'TEXTAREA')
  132. return element.value;
  133. else
  134. return element.innerHTML;
  135. };
  136.  
  137. var set_element_value = function(element, value)
  138. {
  139. if (element.nodeName == 'TEXTAREA')
  140. element.value = value;
  141. else
  142. element.innerHTML = value;
  143. };
  144.  
  145. var commit_backup = function(element)
  146. {
  147. var element_value = get_element_value(element);
  148.  
  149. // backup if value is not empty
  150. if (!/^\s*$/.test(element_value))
  151. {
  152. var bak_payload = append_timestamp(element_value);
  153. GM_setValue(get_element_key(element), bak_payload);
  154. }
  155. };
  156.  
  157. var confirm_restore;
  158.  
  159. var get_backup_content = function(element)
  160. {
  161. // backup payload is in format of "backup_text@save_time",
  162. // where save_time is the millisecond from Javascript Date object's getTime()
  163. var bak_payload = GM_getValue(get_element_key(element));
  164. if (!bak_payload)
  165. return false;
  166.  
  167. var bak_content = remove_timestamp(bak_payload);
  168. // ignore if backup text is identical to current value
  169. if (bak_content == get_element_value(element))
  170. return false;
  171. else
  172. return bak_content;
  173. };
  174.  
  175. var restore_backup = function(elements, index)
  176. {
  177. // check with user before overwriting existing content with backup
  178. // asynchronized when confirmation is enabled, synchronized otherwise
  179. if (confirm_overwrite)
  180. {
  181. var bak_content = get_backup_content(elements[index]);
  182. if (bak_content !== false)
  183. {
  184. confirm_restore(elements, index, bak_content);
  185. }
  186. }
  187. else
  188. {
  189. for (var i = 0; i < elements.length; ++i)
  190. {
  191. var element = elements[i];
  192. set_element_value(element, get_backup_content(element));
  193. }
  194. }
  195. };
  196.  
  197. confirm_restore = function(elements, index, bak_content)
  198. {
  199. var element = elements[index];
  200. element.scrollIntoView(false);
  201.  
  202. // flash the element
  203. var ori_border = element.style.border;
  204. var new_border = '2px solid red';
  205. var toggle = true;
  206. var flashed = flash_count;
  207. var interval_id;
  208.  
  209. var toggle_border = function()
  210. {
  211. element.style.border = (toggle ? new_border : ori_border);
  212. toggle = !toggle;
  213.  
  214. --flashed;
  215. if (flashed == 0)
  216. {
  217. clearInterval(interval_id);
  218.  
  219. var msg = "[Textarea Backup] Backup exists for this element, proceed to overwrite with this backup?\n\n";
  220. msg += bak_content.length > 750 ?
  221. bak_content.substr(0, 500) + "\n..." :
  222. bak_content;
  223.  
  224. if (confirm(msg))
  225. set_element_value(element, bak_content);
  226.  
  227. if (index + 1 < elements.length)
  228. {
  229. // setTimeout is an asynchronized operation
  230. // need recursion to serialize restoration on elements
  231. restore_backup(elements, index + 1);
  232. }
  233. }
  234. };
  235.  
  236. interval_id = setInterval(toggle_border, flash_frequency);
  237. };
  238.  
  239. var on_focus = function(event)
  240. {
  241. var element = event.target;
  242. var element_id = get_element_id(element);
  243.  
  244. if (!prompted[element_id])
  245. {
  246. // set prompted status disregarding user's choice of overwriting
  247. prompted[element_id] = true;
  248.  
  249. restore_backup([element], 0);
  250. }
  251. };
  252.  
  253. var on_blur = function(event)
  254. {
  255. commit_backup(event.target);
  256. };
  257.  
  258. var on_submit = function(event)
  259. {
  260. for (var i = 0; i < targets.length; ++i)
  261. GM_deleteValue(get_element_key(targets[i]));
  262. };
  263.  
  264. var init_backup = function(element)
  265. {
  266. prompted[get_element_id(element)] = false;
  267.  
  268. element.addEventListener('focus', on_focus, true);
  269.  
  270. // save buffer when the element loses focus
  271. if (blur_backup)
  272. element.addEventListener('blur', on_blur, true);
  273. // delete buffer when the form is submitted
  274. if (!keep_after_submission && element.form)
  275. element.form.addEventListener('submit', on_submit, true);
  276. };
  277.  
  278. var restore_all = function()
  279. {
  280. // restore all targets and set prompted status
  281. for (var i = 0; i < targets.length; ++i)
  282. {
  283. var target = targets[i];
  284. var target_id = get_element_id(target);
  285.  
  286. if (!prompted[target_id])
  287. prompted[target_id] = true;
  288. restore_backup(targets, i);
  289. }
  290. };
  291.  
  292. var backup_dynamic = function(evt)
  293. {
  294. if (evt.target.querySelectorAll == undefined)
  295. return;
  296. var new_textareas = evt.target.querySelectorAll(target_selector);
  297. for (var i = 0; i < new_textareas.length; ++i)
  298. {
  299. var new_textarea = new_textareas.item(i);
  300. targets.push(new_textarea);
  301. init_backup(new_textarea);
  302. }
  303. };
  304.  
  305. // expiration check routine
  306. if (expiry_timespan > 0)
  307. {
  308. // get all associated backups for this page, and compare timestamp now and then
  309. var curr_time = new Date().getTime();
  310. var stored_bak = GM_listValues();
  311.  
  312. for (var stored_bak_index = 0; stored_bak_index < stored_bak.length; ++stored_bak_index)
  313. {
  314. var bak_payload = GM_getValue(stored_bak[stored_bak_index]);
  315. var bak_content = remove_timestamp(bak_payload);
  316. var bak_time = get_timestamp(bak_payload);
  317. if (curr_time - bak_time >= expiry_timespan)
  318. GM_deleteValue(stored_bak[stored_bak_index]);
  319. }
  320. }
  321.  
  322. var query_result = document.querySelectorAll(target_selector);
  323. for (var query_result_index = 0; query_result_index < query_result.length; ++query_result_index)
  324. {
  325. var query_item = query_result.item(query_result_index);
  326. targets.push(query_item);
  327. init_backup(query_item);
  328. }
  329.  
  330. if (targets.length > 0)
  331. {
  332. // save buffer in interval fashion
  333. if (timely_backup_interval > 0)
  334. {
  335. var backup_all = function()
  336. {
  337. for (var i = 0; i < targets.length; ++i)
  338. {
  339. var target = targets[i];
  340. if (prompted[get_element_id(target)])
  341. commit_backup(target);
  342. }
  343. };
  344. setInterval(backup_all, timely_backup_interval);
  345. }
  346. }
  347.  
  348. document.addEventListener('DOMNodeInserted', backup_dynamic);
  349.  
  350. GM_registerMenuCommand('Restore all textareas in this page', restore_all);