Text Highlighter 2015

Automatically highlight user-defined text with Seek function (2015-09-20)

目前為 2015-10-11 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Text Highlighter 2015
  3. // @author erosman and Jefferson "jscher2000" Scher
  4. // @namespace JeffersonScher
  5. // @version 2.0.1
  6. // @description Automatically highlight user-defined text with Seek function (2015-09-20)
  7. // @include https://greasyfork.org/*
  8. // @grant GM_registerMenuCommand
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_getResourceURL
  12. // @copyright Copyright 2015 Jefferson Scher. Portions created by erosman.
  13. // @license BSD 3-clause
  14. // @resource mycon http://www.jeffersonscher.com/gm/src/gfrk-TH15-ver201.png
  15. // ==/UserScript==
  16. var script_about = "https://greasyfork.org/en/scripts/3719-text-highlighter-dynamic";
  17.  
  18. /* --------- Note ---------
  19. TO INCLUDE SITES (only Greasy Fork is initially included):
  20.  
  21. Go to Add-ons - User Scripts (Ctrl+Shift+a/Cmd+Shift+a on Firefox Windows/Mac)
  22. Click on the Script's Option
  23. Under User Settings Tab, Add Included/Excluded Pages that you want the script to run on
  24. Click OK
  25.  
  26. Note from erosman: If you find that another script clashes with this script, set Text Highlighter to Execute first.
  27. Go to Add-ons - User Scripts ('Ctrl+ Shift + a' on Firefox)
  28. Right Click on the Script
  29. On the context menu click: Execute first
  30.  
  31. On Add-ons - User Scripts, you can also Click on the Execution Order (top Right) and
  32. change the execution order so that Text Highlighter runs before those scripts that clashes with it.
  33.  
  34. --------- History ---------
  35. http://userscripts-mirror.org/scripts/show/292083
  36. http://userscripts-mirror.org/topics/187122.html
  37. */
  38.  
  39. (function() { // anonymous function wrapper, used for error checking & limiting scope
  40. 'use strict';
  41. if (window.self !== window.top) { return; } // end execution if in a frame
  42.  
  43. // sample keyword+style object to get started
  44. var hlobjDefault = {
  45. "set100" : {
  46. keywords : "scripts|script",
  47. type: "string",
  48. textcolor : "rgb(0,0,0)",
  49. backcolor : "rgb(255,255,128)",
  50. fontweight : "inherit",
  51. custom : "",
  52. enabled : "true",
  53. visible : "true",
  54. updated : ""
  55. },
  56. "set099" : {
  57. keywords : "site",
  58. type: "word",
  59. textcolor : "rgb(0,0,0)",
  60. backcolor : "rgb(255,192,255)",
  61. fontweight : "inherit",
  62. custom : "",
  63. enabled : "true",
  64. visible : "true",
  65. updated : ""
  66. },
  67. "set098" : {
  68. keywords : "^September \\d{1,2}",
  69. type: "regex",
  70. textcolor : "rgb(0,0,0)",
  71. backcolor : "rgb(192,255,255)",
  72. fontweight : "inherit",
  73. custom : "",
  74. enabled : "true",
  75. visible : "true",
  76. updated : ""
  77. }
  78. };
  79. var kwhieditstyle = ["rgb(0,0,255)","rgb(255,255,0)","inherit",""];
  80.  
  81. // read pref storage: keyword-style sets
  82. var hljson = GM_getValue("kwstyles");
  83. if (!hljson || hljson.length == 0){
  84. var hlobj = hlobjDefault;
  85. // check for legacy preferences
  86. var kwold = GM_getValue("keywords");
  87. if (kwold) if(kwold.length > 0) {
  88. hlobj.set100.keywords = kwold.split(',').join('|');
  89. }
  90. var hlold = GM_getValue("highlightStyle");
  91. if (hlold) if(hlold.length > 0) {
  92. // really should try to parse this, but for now...
  93. hlobj.set100.custom = hlold;
  94. }
  95. // save starting values
  96. hljson = JSON.stringify(hlobj);
  97. GM_setValue("kwstyles",hljson);
  98. } else {
  99. var hlobj = JSON.parse(hljson);
  100. }
  101. // global keys array
  102. var hlkeys = Object.keys(hlobj);
  103.  
  104. // read/set other prefs
  105. var hlbtnvis = GM_getValue("hlbtnvis");
  106. if (!hlbtnvis){
  107. hlbtnvis = "on";
  108. GM_setValue("hlbtnvis",hlbtnvis);
  109. }
  110. var hlprecode = GM_getValue("hlprecode");
  111. if (!hlprecode){
  112. hlprecode = true;
  113. GM_setValue("hlprecode",hlprecode);
  114. }
  115. var hlnextset = GM_getValue("hlnextset");
  116. if (!hlnextset){
  117. hlnextset = 101;
  118. GM_setValue("hlnextset",hlnextset);
  119. }
  120. // Inject CSS
  121. function insertCSS(setkeys){
  122. for (var j = 0; j < setkeys.length; ++j){
  123. var hlset = setkeys[j];
  124. if (hlobj[hlset].visible == "true"){
  125. var rule = "."+hlset+"{display:inline!important;";
  126. if (hlobj[hlset].textcolor.length > 0) rule += "color:"+hlobj[hlset].textcolor+";";
  127. if (hlobj[hlset].backcolor.length > 0) rule += "background-color:"+hlobj[hlset].backcolor+";";
  128. if (hlobj[hlset].fontweight.length > 0) rule += "font-weight:"+hlobj[hlset].fontweight+";";
  129. if (hlobj[hlset].custom.length > 0) rule += hlobj[hlset].custom+";";
  130. rule += "}";
  131. var setrule = document.querySelector('style[hlset="' + hlset +'"]');
  132. if (!setrule){
  133. var s = document.createElement("style");
  134. s.type = "text/css";
  135. s.setAttribute("hlset", hlset);
  136. s.appendChild(document.createTextNode(rule));
  137. document.body.appendChild(s);
  138. } else {
  139. setrule.innerHTML = rule;
  140. }
  141. }
  142. }
  143. }
  144. insertCSS(hlkeys);
  145.  
  146. // Main workhorse routine
  147. function THmo_doHighlight(el,subset){
  148. if (subset) var keyset = subset;
  149. else var keyset = hlkeys;
  150. for (var j = 0; j < keyset.length; ++j) {
  151. var hlset = keyset[j];
  152. if (hlobj[hlset].visible == "true" && hlobj[hlset].enabled == "true"){
  153. var hlkeywords = hlobj[hlset].keywords;
  154. if (hlkeywords.length > 0) {
  155. if (hlobj[hlset].type != "regex"){
  156. var rQuantifiers = /[-\/\\^$*+?.()[\]{}]/g;
  157. hlkeywords = hlkeywords.replace(rQuantifiers, '\\$&');
  158. if (hlobj[hlset].type == "word"){
  159. hlkeywords = "\\b" + hlkeywords.replace(/\|/g, "\\b|\\b") + "\\b";
  160. }
  161. }
  162. //console.log("hlset:"+hlset+"\nhlkeywords:"+hlkeywords);
  163. var pat = new RegExp('(' + hlkeywords + ')', 'gi');
  164. var span = document.createElement('thdfrag');
  165. span.setAttribute("thdcontain","true");
  166. // getting all text nodes with a few exceptions
  167. if (hlprecode){
  168. var snapElements = document.evaluate(
  169. './/text()[normalize-space() != "" ' +
  170. 'and not(ancestor::style) ' +
  171. 'and not(ancestor::script) ' +
  172. 'and not(ancestor::textarea) ' +
  173. 'and not(ancestor::div[@id="thdtopbar"]) ' +
  174. 'and not(ancestor::div[@id="kwhiedit"]) ' +
  175. 'and not(parent::thdfrag[@txhidy15])]',
  176. el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  177. } else {
  178. var snapElements = document.evaluate(
  179. './/text()[normalize-space() != "" ' +
  180. 'and not(ancestor::style) ' +
  181. 'and not(ancestor::script) ' +
  182. 'and not(ancestor::textarea) ' +
  183. 'and not(ancestor::pre) ' +
  184. 'and not(ancestor::code) ' +
  185. 'and not(ancestor::div[@id="thdtopbar"]) ' +
  186. 'and not(ancestor::div[@id="kwhiedit"]) ' +
  187. 'and not(parent::thdfrag[@txhidy15])]',
  188. el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  189. }
  190.  
  191. if (!snapElements.snapshotItem(0)) { break; }
  192.  
  193. for (var i = 0, len = snapElements.snapshotLength; i < len; i++) {
  194. var node = snapElements.snapshotItem(i);
  195. // check if it contains the keywords
  196. if (pat.test(node.nodeValue)) {
  197. // create an element, replace the text node with an element
  198. var sp = span.cloneNode(true);
  199. sp.innerHTML = node.nodeValue.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(pat, '<thdfrag class="THmo '+hlset+'" txhidy15="'+hlset+'">$1</thdfrag>');
  200. node.parentNode.replaceChild(sp, node);
  201. // try to un-nest containers
  202. if (sp.parentNode.hasAttribute("thdcontain")) sp.outerHTML = sp.innerHTML;
  203. }
  204. }
  205. }
  206. }
  207. }
  208. }
  209. // first run
  210. THmo_doHighlight(document.body,null);
  211. // Add MutationObserver to catch content added dynamically
  212. var THmo_MutOb = (window.MutationObserver) ? window.MutationObserver : window.WebKitMutationObserver;
  213. if (THmo_MutOb){
  214. var THmo_chgMon = new THmo_MutOb(function(mutationSet){
  215. mutationSet.forEach(function(mutation){
  216. for (var i=0; i<mutation.addedNodes.length; i++){
  217. if (mutation.addedNodes[i].nodeType == 1){
  218. THmo_doHighlight(mutation.addedNodes[i],null);
  219. }
  220. }
  221. });
  222. });
  223. // attach chgMon to document.body
  224. var opts = {childList: true, subtree: true};
  225. THmo_chgMon.observe(document.body, opts);
  226. }
  227.  
  228. // Set up top highlight/seek bar
  229. var kwhibar = document.createElement("div");
  230. kwhibar.id = "thdtopbar";
  231. if (hlbtnvis == "on") var btnchk = " checked=\"checked\"";
  232. if (hlprecode) var btnprecode = " checked=\"checked\"";
  233. else var btnchk = "";
  234. kwhibar.innerHTML = "<form id=\"thdtopform\" onsubmit=\"return false\"><p id=\"thdtopbarhome\"><a href=\"" + script_about + "\" target=\"_blank\" title=\"Go to script install page\">JS</a></p>" +
  235. "<div id=\"thdtopcurrent\"><p id=\"thdtopkeywords\" title=\"Click to View, Edit, Seek, or Add Keywords\">Click here to manage keyword/highlight sets</p>" +
  236. "<div id=\"thdtopdrop\" style=\"display:none;\"><div id=\"thdtable\"><table cellspacing=\"0\"><tbody id=\"kwhitbod\"></tbody></table></div><p><button id=\"btnkwhiadd\">Add New Set</button>" +
  237. "<span style=\"float:right\"><button id=\"btnkwhiexport\">Export Sets</button> [?Import?] <button class=\"btnkwhiclose\" onclick=\"document.getElementById('thdtopdrop').style.display='none';return false;\">X</button></span></p></div></div>" +
  238. "<div id=\"thdtopfindbuttons\"><button title=\"First match\" thdaction=\"f\"><b>l</b>&#x25c0;</button> <button title=\"Previous match\" thdaction=\"p\">&#x25c0;</button> <span id=\"thdseekdesc\">Seek</span> <button title=\"Next match\" thdaction=\"n\">&#x25b6;</button> <button title=\"Last match\" thdaction=\"l\">&#x25b6;<b>l</b></button><div id=\"thdseekfail\"></div></div>" +
  239. "<div id=\"thdtopoptions\"><div>Options</div><ul><li><label title=\"Float a button in the upper right corner of the document to quickly access this panel\"><input type=\"checkbox\" id=\"chkhbtn\"" + btnchk +
  240. "> Show H button</label></li><li><label title=\"Highlight matches in &lt;pre&gt; and &lt;code&gt; tags\"><input type=\"checkbox\" id=\"chkprecode\"" + btnprecode +
  241. "> Match in pre/code</label></li></ul></div>" +
  242. "<button class=\"btnkwhiclose\" onclick=\"document.getElementById('thdtopbar').style.display='none';document.getElementById('thdtopspacer').style.display='none';return false;\" style=\"float:right\">X</button></form>" +
  243. "<style type=\"text/css\">#thdtopbar{position:fixed;top:0;left:0;height:26px;width:100%;padding:0;color:#024;background:#ddd;font-family:sans-serif;font-size:16px;line-height:16px;border-bottom:1px solid #024;z-index:2500;display:none} " +
  244. "#thdtopbar,#thdtopbar *{box-sizing:content-box;} #thdtopform{display:block;position:relative;float:left;width:100%;margin:0;border:none;} " +
  245. "#thdtopbarhome,#thdtopcurrent,#thdtopfindbuttons,#thdtopoptions{float:left;top:0;left:0;margin:0;padding:5px 8px 4px;border-right:1px solid #fff;font-size:16px;} " +
  246. "#thdtopbarhome{width:22px;text-align:center;overflow:hidden;} #thdtopbarhome a{display:block;} #thdtopbarhome img{display:block;border:none;border-radius:3px;padding:3px;margin:-3px 0 -4px 0;background-color:#fff} " +
  247. "#thdtopfindbuttons{padding-bottom:1px;position:relative} #thdtopfindbuttons button{margin:-5px 0 -2px 0;width:36px;height:22px;color:#024;background:#f0f0f0;border:1px solid #024;border-radius:4px;padding:1px 3px;} " +
  248. "#thdtopfindbuttons button:hover{background:#ffa;} #thdseekdesc{cursor:pointer} #thdtopkeywords{margin:0;width:500px;cursor:pointer;} " +
  249. "#thdseekfail{display:none;position:absolute;top:30px;left:15px;z-index:2001;width:200px;color:#f8f8f8;background:#b00;border-radius:6px;text-align:center;font-size:12px;padding:3px}" +
  250. "#thdtopkeywords span{display:inline-block;width:100%;overflow:hidden;text-overflow:ellipsis;} #thdtable{max-height:600px;overflow-y:auto;overflow-x:hidden} " +
  251. "#thdtopdrop{position:absolute;top:26px;left:38px;width:500px;margin:0 -1px 0 -1px;padding:0 8px 8px 8px;background:#ddd;border:1px solid #024;border-top:none;border-radius:0 0 6px 6px;} " +
  252. "#thdtopdrop table{width:100%;background:#fff;border-top:1px solid #000;border-left:1px solid #000;table-layout:fixed} " +
  253. "#thdtopdrop td{padding:4px 4px; vertical-align:top;border-right:1px solid #000;border-bottom:1px solid #000;} #thdtopdrop td div{word-wrap:break-word} #thdtopdrop p{margin-top:8px;margin-bottom:0;} " +
  254. "#thdtopoptions{position:relative;width:160px;height:26px;padding:0 8px;} #thdtopoptions > div{padding:5px 0 4px;} " +
  255. "#thdtopoptions ul{position:absolute;top:26px;left:0;width:160px;margin:0 -1px 0 -1px;padding:0 8px 8px 8px;background:#ddd;border:1px solid #024;border-top:none;border-radius:0 0 6px 6px;list-style:none;} " +
  256. "#thdtopoptions li{width:100%;float:left;padding:2px 0;} #thdtopoptions ul{display:none;} #thdtopoptions:hover ul{display: block;border:1px solid #024;border-top:none;} #thdtopoptions li:hover{background:#eee;}" +
  257. ".btnkwhiclose{float:right;font-size:11px;margin-top:2px;} .thdtype{color:#ccc;float:right;font-size:12px;padding-top:8px;} #thdtopbar label{font-weight:normal;display:inline;margin:0}</style>";
  258. document.body.appendChild(kwhibar);
  259. // Attach event handlers
  260. document.getElementById("thdtopkeywords").addEventListener("click",thddroptoggle,false);
  261. document.getElementById("kwhitbod").addEventListener("click",kwhiformevent,false);
  262. document.getElementById("btnkwhiadd").addEventListener("click",kwhinewset,false);
  263. document.getElementById("btnkwhiexport").addEventListener("click",kwhiexport,false);
  264. document.getElementById("thdtopfindbuttons").addEventListener("click",thdseek,false);
  265. document.getElementById("chkhbtn").addEventListener("click",kwhihbtn,false);
  266. document.getElementById("chkprecode").addEventListener("click",kwhiprecode,false);
  267. // Add spacer at top of body
  268. var divsp = document.createElement("div");
  269. divsp.id = "thdtopspacer";
  270. divsp.setAttribute("style","clear:both;display:none");
  271. divsp.style.height = parseInt(27 - parseInt(window.getComputedStyle(document.body,null).getPropertyValue("margin-top"))) + "px";
  272. document.body.insertBefore(divsp, document.body.childNodes[0]);
  273. // Switch JS text to icon
  274. var JSBTN = document.createElement("img");
  275. JSBTN.src = GM_getResourceURL("mycon");
  276. document.querySelector("#thdtopbar a").textContent = "";
  277. document.querySelector("#thdtopbar a").appendChild(JSBTN);
  278. // Add menu item
  279. GM_registerMenuCommand("Show Text Highlighter Bar - View, Edit, Add Keywords and Styles", editKW);
  280. // Inject H button
  281. if (hlbtnvis == "off") var hbtndisp = ' style="display:none"';
  282. else hbtndisp = '';
  283. var dNew = document.createElement("div");
  284. dNew.innerHTML = '<button id="btnshowkwhi"' + hbtndisp + '>H</button><style type="text/css">#btnshowkwhi{position:fixed;top:4px;right:4px;opacity:0.2;' +
  285. 'color:#000;background-color:#ffa;font-weight:bold;font-size:12px;border:1px solid #ccc;border-radius:4px;padding:2px 3px;z-index:1999;min-width:22px;min-height:22px}' +
  286. '#btnshowkwhi:hover{opacity:0.8}@media print{#btnshowkwhi{display:none;}}</style>';
  287. document.body.appendChild(dNew);
  288. document.getElementById("btnshowkwhi").addEventListener("click",editKW,false);
  289. function editKW(e){
  290. refreshSetList();
  291. // show form
  292. document.getElementById("thdtopbar").style.display = "block";
  293. document.getElementById("thdtopspacer").style.display = "block";
  294. }
  295. function thdDropSetList(e){
  296. refreshSetList();
  297. document.getElementById("thdtopdrop").style.display = "block";
  298. }
  299. function thddroptoggle(e){
  300. if (document.getElementById("thdtopdrop").style.display == "none") thdDropSetList();
  301. else document.getElementById("thdtopdrop").style.display = "none";
  302. }
  303. function refreshSetList(e){
  304. // clear old rows from form
  305. document.getElementById("kwhitbod").innerHTML = "";
  306. // populate data - hlobj is global
  307. for (var j = 0; j < hlkeys.length; ++j){
  308. var hlset = hlkeys[j];
  309. if (hlobj[hlset].visible == "true"){
  310. if (hlobj[hlset].enabled == "true") var strchk = ' checked=\"checked\"';
  311. else var strchk = '';
  312. var newrow = document.createElement("tr");
  313. var thdtypenote = '';
  314. newrow.setAttribute("kwhiset", hlset);
  315. if(hlobj[hlset].type != "string"){
  316. thdtypenote = '<span class="thdtype">' + hlobj[hlset].type + '</span>';
  317. }
  318. if (j == 0){
  319. newrow.innerHTML = '<td style=\"width:286px\"><div class=\"' + hlset + '\">' + hlobj[hlset].keywords + '</div>' + thdtypenote + '</td>' +
  320. '<td style=\"width:195px\"><button kwhiset=\"' + hlset + '\" title=\"Bring matches into view\">Seek</button> ' +
  321. '<button kwhiset=\"' + hlset + '\">Edit</button> <label><input type=\"checkbox\" kwhiset=\"' + hlset +
  322. '\"' + strchk + '"> Enabled </label></td>';
  323. } else {
  324. newrow.innerHTML = '<td><div class=\"' + hlset + '\">' + hlobj[hlset].keywords + '</div>' + thdtypenote + '</td>' +
  325. '<td><button kwhiset=\"' + hlset + '\" title=\"Bring matches into view\">Seek</button> ' +
  326. '<button kwhiset=\"' + hlset + '\">Edit</button> <label><input type=\"checkbox\" kwhiset=\"' + hlset +
  327. '\"' + strchk + '"> Enabled </label></td>';
  328. }
  329. document.getElementById("kwhitbod").appendChild(newrow);
  330. }
  331. }
  332. }
  333. function kwhiformevent(e){
  334. if (e.target.nodeName == "INPUT"){ // Enabled checkbox
  335. var hlsetnum = e.target.getAttribute("kwhiset");
  336. kwhienabledisable(hlsetnum, e.target.checked);
  337. }
  338. if (e.target.nodeName == "BUTTON"){ // Call up edit form or find bar
  339. var hlset = e.target.getAttribute('kwhiset');
  340. if (e.target.textContent == "Edit"){
  341. // set set number attribute
  342. document.querySelector('#kwhiedit tr').setAttribute('kwhiset', hlset);
  343. // set class for keywords
  344. document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className = hlset;
  345. // enter placeholder text & type
  346. document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent = hlobj[hlset].keywords;
  347. document.getElementById("kwhipattype").selectedIndex = 0;
  348. if (hlobj[hlset].type == "word") document.getElementById("kwhipattype").selectedIndex = 1;
  349. if (hlobj[hlset].type == "regex") document.getElementById("kwhipattype").selectedIndex = 2;
  350. // set style editing to default and override with set rules
  351. kwhieditstyle = ["rgb(0,0,255)","rgb(255,255,0)","inherit",""]; // defaults
  352. if (hlobj[hlset].textcolor.length > 0) kwhieditstyle[0] = hlobj[hlset].textcolor;
  353. if (hlobj[hlset].backcolor.length > 0) kwhieditstyle[1] = hlobj[hlset].backcolor;
  354. if (hlobj[hlset].fontweight.length > 0) kwhieditstyle[2] = hlobj[hlset].fontweight;
  355. if (hlobj[hlset].custom.length > 0) kwhieditstyle[3] = hlobj[hlset].custom;
  356. kwhiShowEditForm();
  357. }
  358. if (e.target.textContent == "Seek"){
  359. // Populate current seek set to #thdtopkeywords
  360. var divDataTD = e.target.parentNode.previousElementSibling;
  361. document.getElementById("thdtopkeywords").innerHTML = "<i>Seeking:</i> " + divDataTD.firstChild.outerHTML;
  362. // Store set to seek in #thdtopfindbuttons
  363. document.getElementById("thdtopfindbuttons").setAttribute("thdseek", hlset);
  364. // Close Keyword Sets form
  365. document.getElementById('thdtopdrop').style.display='none';
  366. // Send click event to the "seek first" button
  367. document.getElementById('thdtopfindbuttons').children[0].click();
  368. }
  369. }
  370. }
  371. function kwhienabledisable(hlsetnum,enable){
  372. if (enable == false) {
  373. // Update object and persist to GM storage
  374. hlobj[hlsetnum].enabled = "false";
  375. hljson = JSON.stringify(hlobj);
  376. GM_setValue("kwstyles",hljson);
  377. // Unhighlight
  378. unhighlight(hlsetnum);
  379. // Clear seek info from bar if this set is there
  380. var seekset = document.getElementById("thdtopfindbuttons").getAttribute("thdseek");
  381. if (seekset){
  382. if(seekset.indexOf("|") > -1) seekset = seekset.split("|")[0];
  383. if (hlsetnum == seekset){
  384. document.getElementById("thdtopfindbuttons").setAttribute("thdseek","");
  385. document.getElementById("thdseekdesc").textContent = "Seek";
  386. document.getElementById("thdtopkeywords").innerHTML = "Click here to manage keyword/highlight sets";
  387. }
  388. }
  389. } else {
  390. // Update object and persist to GM storage
  391. hlobj[hlsetnum].enabled = "true";
  392. hljson = JSON.stringify(hlobj);
  393. GM_setValue("kwstyles",hljson);
  394. // Highlight
  395. THmo_doHighlight(document.body,[hlsetnum]);
  396. }
  397. }
  398. function kwhinewset(e,kwtext){ // call up new set form
  399. // set set number attribute
  400. document.querySelector('#kwhiedit tr').setAttribute('kwhiset', 'new');
  401. // clear class for keywords
  402. document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className = "";
  403. // enter placeholder text & default type
  404. if (kwtext) document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent = kwtext;
  405. else document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent = "larry|moe|curly";
  406. document.getElementById("kwhipattype").selectedIndex = 0;
  407. // set style editing to defaults
  408. kwhieditstyle = ["rgb(0,0,255)","rgb(255,255,0)","inherit",""];
  409. kwhiShowEditForm();
  410. }
  411. function kwhiShowEditForm(){
  412. var rule = "#stylecontrols>p>span{";
  413. if (kwhieditstyle[0].length > 0) rule += "color:"+kwhieditstyle[0]+";";
  414. if (kwhieditstyle[1].length > 0) rule += "background-color:"+kwhieditstyle[1]+";";
  415. if (kwhieditstyle[2].length > 0) rule += "font-weight:"+kwhieditstyle[2]+";";
  416. if (kwhieditstyle[3].length > 0) rule += kwhieditstyle[3]+";";
  417. document.getElementById("kwhiedittemp").innerHTML = rule + "}";
  418. populateRGB("txt",kwhieditstyle[0]);
  419. populateRGB("bkg",kwhieditstyle[1]);
  420. document.getElementById("fwsel").value = kwhieditstyle[2];
  421. document.getElementById("kwhicustom").value = kwhieditstyle[3];
  422. // show form
  423. document.getElementById("kwhiedit").style.display = "block";
  424. }
  425. function kwhiexport(e){
  426. prompt("JSON data\nPress Ctrl+c or right-click to copy\n ", JSON.stringify(hlobj));
  427. }
  428. function kwhihbtn(e){
  429. if (e.target.checked == false){
  430. hlbtnvis = "off";
  431. GM_setValue("hlbtnvis",hlbtnvis);
  432. document.getElementById("btnshowkwhi").style.display = "none";
  433. } else {
  434. hlbtnvis = "on";
  435. GM_setValue("hlbtnvis",hlbtnvis);
  436. document.getElementById("btnshowkwhi").style.display = "";
  437. }
  438. }
  439. function kwhiprecode(e){
  440. if (e.target.checked == false){
  441. // Update var, persist the preference, unhighlight, rehighlight
  442. hlprecode = false;
  443. GM_setValue("hlprecode",hlprecode);
  444. unhighlight(null);
  445. THmo_doHighlight(document.body);
  446. } else {
  447. // Update var, persist the preference, rehighlight
  448. hlprecode = true;
  449. GM_setValue("hlprecode",hlprecode);
  450. THmo_doHighlight(document.body);
  451. }
  452. }
  453. function thdseek(e){
  454. if (e.target.nodeName == "DIV") return; // ignore background clicks
  455. var seekset = e.currentTarget.getAttribute("thdseek");
  456. if (!seekset){ // user needs to select a set to seek in
  457. thdDropSetList();
  458. } else {
  459. var seekparams = seekset.split("|");
  460. var seekmatches = document.querySelectorAll('thdfrag[txhidy15="'+seekparams[0]+'"]');
  461. // Update or add total size of set; FIGURE OUT LATER: what if this changed??
  462. seekparams[1] = seekmatches.length;
  463. if (seekmatches.length > 0){
  464. if (e.target.nodeName == "THDFRAG"){ // re-scroll to the current reference
  465. thdshow(seekmatches[parseInt(seekparams[2])]);
  466. } else { // BUTTON
  467. var seekaction = e.target.getAttribute("thdaction");
  468. if (!seekaction) seekaction = "f";
  469. if (seekparams.length == 3){ // User has seeked in this set
  470. switch (seekaction){
  471. case "f":
  472. seekparams[2] = 0;
  473. var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
  474. if (rtn == false) seekagain("n");
  475. break;
  476. case "p":
  477. if (parseInt(seekparams[2]) > 0) {
  478. seekparams[2] = parseInt(seekparams[2]) - 1;
  479. var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
  480. if (rtn == false){
  481. if (parseInt(seekparams[2]) > 0) seekagain("p");
  482. else seekfailnotc("No previous match visible");
  483. }
  484. } else {
  485. seekfailnotc("Already reached first match");
  486. }
  487. break;
  488. case "n":
  489. if (parseInt(seekparams[2]) < (seekmatches.length-1)) {
  490. seekparams[2] = parseInt(seekparams[2]) + 1;
  491. var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
  492. if (rtn == false){
  493. if (parseInt(seekparams[2]) < (seekmatches.length-1)) seekagain("n");
  494. else seekfailnotc("No later match visible");
  495. }
  496. } else {
  497. seekparams[2] = (seekmatches.length-1); // in case it's too high, fix that here
  498. seekfailnotc("Already reached last match");
  499. }
  500. break;
  501. case "l":
  502. seekparams[2] = (seekmatches.length-1);
  503. var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
  504. if (rtn == false) seekagain("p");
  505. break;
  506. }
  507. } else {
  508. seekparams[2] = 0;
  509. thdshow(seekmatches[parseInt(seekparams[2])]);
  510. }
  511. document.getElementById("thdtopfindbuttons").setAttribute("thdseek", seekparams.join("|"));
  512. document.getElementById("thdseekdesc").textContent = (parseInt(seekparams[2])+1) + " of " + seekparams[1];
  513. }
  514. } else {
  515. document.getElementById("thdseekdesc").textContent = "0 of 0";
  516. }
  517. }
  518. }
  519. function thdshow(elt){ // this could be much prettier with animation!
  520. elt.scrollIntoView();
  521. var rect = elt.getClientRects()[0];
  522. if (rect){ // scroll down if behind the control bar
  523. if (rect.top < 27) window.scroll(0, window.scrollY-27);
  524. return true;
  525. } else { // match is not visible
  526. return false;
  527. }
  528. }
  529. function seekagain(dir){
  530. switch (dir){
  531. case "p":
  532. seekfailnotc("Hidden, trying previous match...");
  533. window.setTimeout(function(){document.querySelector('button[thdaction="p"]').click();},250);
  534. break;
  535. case "n":
  536. seekfailnotc("Hidden, trying next match...");
  537. window.setTimeout(function(){document.querySelector('button[thdaction="n"]').click();},250);
  538. break;
  539. }
  540. }
  541. var evttimer;
  542. function seekfailnotc(txt){
  543. var sfdiv = document.getElementById("thdseekfail");
  544. sfdiv.textContent = txt;
  545. sfdiv.style.display = "block";
  546. if (evttimer) window.clearTimeout(evttimer);
  547. evttimer = window.setTimeout(function(){document.getElementById("thdseekfail").style.display="none";}, 800);
  548. }
  549. function unhighlight(setnum){
  550. if (setnum) var tgts = document.querySelectorAll('thdfrag[txhidy15="' + setnum + '"]');
  551. else var tgts = document.querySelectorAll('thdfrag[txhidy15]'); // remove ALL
  552. for (var i=0; i<tgts.length; i++){
  553. // Check for co-extant parent(s) to remove potentially stranded <span>s
  554. var parnode = tgts[i].parentNode, parpar = parnode.parentNode, tgtspan;
  555. if (parnode.hasAttribute("thdcontain") && parnode.innerHTML == tgts[i].outerHTML){
  556. parnode.outerHTML = tgts[i].textContent.replace(/</g, '&lt;').replace(/>/g, '&gt;');
  557. tgtspan = parpar;
  558. } else {
  559. tgts[i].outerHTML = tgts[i].textContent.replace(/</g, '&lt;').replace(/>/g, '&gt;');
  560. tgtspan = parnode;
  561. }
  562. tgtspan.normalize();
  563. if (tgtspan.hasAttribute("thdcontain")){
  564. parnode = tgtspan.parentNode;
  565. if (parnode){
  566. if (parnode.hasAttribute("thdcontain") && parnode.innerHTML == tgtspan.outerHTML && tgtspan.querySelectorAll('thdfrag[txhidy15]').length == 0){
  567. parnode.outerHTML = tgtspan.innerHTML;
  568. } else if (parnode.innerHTML == tgtspan.outerHTML && tgtspan.querySelectorAll('thdfrag[txhidy15]').length == 0) {
  569. parnode.innerHTML = tgtspan.innerHTML;
  570. }
  571. }
  572. }
  573. }
  574. }
  575. // Set up add/edit form
  576. var kwhied = document.createElement("div");
  577. kwhied.id = "kwhiedit";
  578. kwhied.innerHTML = "<form onsubmit=\"return false;\"><p style=\"margin-top:0\"><b>Edit/Add Keywords/Highlighting</b>" +
  579. "<button class=\"btnkwhiclose\" onclick=\"document.getElementById('kwhiedit').style.display='none'; return false;\">X</button>" +
  580. "</p><p>List longer forms of a word first to match both in full. Example: \"children|child\" will highlight both, but \"child|children\" " +
  581. "will only highlight child, it won't expand the selection to children.</p>" +
  582. "<table cellspacing=\"0\" style=\"table-layout:fixed\"><tbody><tr kwhiset=\"new\"><td style=\"width:45%\">" +
  583. "<p contenteditable=\"true\" style=\"border:1px dotted #000;word-wrap:break-word;display:block!important\" class=\"\">placeholder</p>" +
  584. "<p style=\"margin-top:2em\">Match type: <select id=\"kwhipattype\"><option value=\"string\" selected>Anywhere in a word</option>" +
  585. "<option value=\"word\">\"Whole\" words only</option><option value=\"regex\">Regular Expression (advanced)</option></select></p></td>" +
  586. "<td style=\"width:55%\" id=\"stylecontrols\"><p><span>Text color:</span> R:<input id=\"txtr\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" " +
  587. "style=\"width:4em\" value=\"0\"> G:<input id=\"txtg\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" " +
  588. "style=\"width:4em\" value=\"0\"> B:<input id=\"txtb\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" " +
  589. "style=\"width:4em\" value=\"0\"> <button id=\"btntxtreset\">Reset</button></p><p><span>Background:</span> R:<input id=\"bkgr\" " +
  590. "type=\"number\" min=\"0\" max=\"255\" step=\"1\" style=\"width:4em\" value=\"255\"> G:<input id=\"bkgg\" " +
  591. "type=\"number\" min=\"0\" max=\"255\" step=\"1\" style=\"width:4em\" value=\"255\"> B:<input id=\"bkgb\" " +
  592. "type=\"number\" min=\"0\" max=\"255\" step=\"1\" style=\"width:4em\" value=\"128\"> <button id=\"btnbkgreset\">Reset</button>" +
  593. "</p><p><span>Font-weight:</span> <select id=\"fwsel\"><option value=\"inherit\" selected>inherit</option>" +
  594. "<option value=\"bold\"><b>bold</b></option><option value=\"normal\">not bold</option></select></p><p><span>Custom:</span> <input type=\"text\" " +
  595. "id=\"kwhicustom\" style=\"width:55%\"> <button id=\"kwhicustomapply\">Apply</button></p></td></tr></tbody></table>" +
  596. "<p><button id=\"btnkwhisave\">Save Changes</button> <button id=\"btnkwhicancel\">Discard Changes</button> <button id=\"btnkwhiremove\">Hide Set</button></p></form><style type=\"text/css\">" +
  597. "#kwhiedit{position:fixed;top:1px;left:150px;width:800px;height:400px;border:1px solid #000;border-radius:6px;padding:1em;color:#000;" +
  598. "background:#fafafa;z-index:2501;display:none} #kwhiedit table{width:100%;background:#fff;border-top:1px solid #000;" +
  599. "border-left:1px solid #000;} #kwhiedit td{padding:0 16px; vertical-align:top;border-right:1px solid #000;border-bottom:1px solid #000;}" +
  600. "#stylecontrols>p>span{display:inline-block;width:6.5em;}</style><style type=\"text/css\" id=\"kwhiedittemp\"></style></div>";
  601. document.body.appendChild(kwhied);
  602. // Attach event handlers
  603. document.getElementById("btnkwhisave").addEventListener("click",kwhisavechg,false);
  604. document.getElementById("btnkwhicancel").addEventListener("click",kwhicancel,false);
  605. document.getElementById("btnkwhiremove").addEventListener("click",kwhiremove,false);
  606. document.getElementById("stylecontrols").addEventListener("input", updatestyle, false);
  607. document.getElementById("btntxtreset").addEventListener("click",kwhicolorreset,false);
  608. document.getElementById("btnbkgreset").addEventListener("click",kwhicolorreset,false);
  609. document.getElementById("fwsel").addEventListener("change",kwhifwchg,false);
  610. document.getElementById("kwhicustomapply").addEventListener("click",kwhicustom,false);
  611.  
  612. function kwhisavechg(e){
  613. // Update object, regenerate CSS if applicable, apply to document
  614. var hlset = document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className;
  615. var kwtext = document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent;
  616. if (hlset == ""){
  617. // create a new set number
  618. var hlset = "set" + hlnextset;
  619. hlnextset += 1;
  620. GM_setValue("hlnextset",hlnextset);
  621. // add the set
  622. if (document.getElementById("kwhipattype").value == "regex") kwtext = kwtext.replace(/\\/g, "\\");
  623. hlobj[hlset] = {
  624. keywords : kwtext,
  625. type: document.getElementById("kwhipattype").value,
  626. textcolor : kwhieditstyle[0],
  627. backcolor : kwhieditstyle[1],
  628. fontweight : kwhieditstyle[2],
  629. custom : kwhieditstyle[3],
  630. enabled : "true",
  631. visible : "true",
  632. updated : ""
  633. }
  634. // Update the global key array
  635. hlkeys = Object.keys(hlobj);
  636. } else {
  637. hlobj[hlset].type = document.getElementById("kwhipattype").value;
  638. // Save keyword changes after user confirmation
  639. if (kwtext != hlobj[hlset].keywords){
  640. if (confirm("Save updated keywords (and other changes)?")){
  641. if (hlobj[hlset].type != "regex") hlobj[hlset].keywords = kwtext;
  642. else hlobj[hlset].keywords = kwtext.replace(/\\/g, "\\");
  643. } else return;
  644. }
  645. // Save style changes without confirmation
  646. hlobj[hlset].textcolor = kwhieditstyle[0];
  647. hlobj[hlset].backcolor = kwhieditstyle[1];
  648. hlobj[hlset].fontweight = kwhieditstyle[2];
  649. hlobj[hlset].custom = kwhieditstyle[3];
  650. // Set updated date/time
  651. hlobj[hlset].updated = (new Date()).toJSON();
  652. }
  653. // Persist the object
  654. hljson = JSON.stringify(hlobj);
  655. GM_setValue("kwstyles",hljson);
  656. // Update CSS rule and parent form
  657. insertCSS([hlset]);
  658. refreshSetList();
  659. // Unhighlight, re-highlight, close dialog
  660. unhighlight(hlset);
  661. THmo_doHighlight(document.body,[hlset])
  662. document.getElementById('kwhiedit').style.display='none';
  663. }
  664. function kwhicancel(e){
  665. // Close dialog (fields will be refresh if it is opened again)
  666. document.getElementById('kwhiedit').style.display='none';
  667. }
  668. function kwhiremove(e){
  669. var hlset = document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className;
  670. if (hlset == ""){
  671. alert("This set has not been saved and therefore does not need to be hidden, you can just close the dialog to discard it.");
  672. } else {
  673. if (confirm("Are you sure you want to hide this set instead of editing it to your own liking?")){
  674. hlobj[hlset].visible = "false";
  675. hlobj[hlset].updated = (new Date()).toJSON();
  676. // Persist the object
  677. hljson = JSON.stringify(hlobj);
  678. GM_setValue("kwstyles",hljson);
  679. // Update set list, remove highlighting, close form
  680. refreshSetList();
  681. unhighlight(hlset);
  682. document.getElementById('kwhiedit').style.display='none';
  683. }
  684. }
  685. }
  686. function kwhicolorreset(e){
  687. // what set is this?
  688. var set = document.querySelector('#kwhiedit tr').getAttribute('kwhiset');
  689. // check which button, reset the RGB
  690. if (e.target.id == "btntxtreset"){
  691. if (set == "new"){
  692. kwhieditstyle[0] = "rgb(0,0,255)";
  693. } else {
  694. kwhieditstyle[0] = hlobj[set].textcolor;
  695. }
  696. populateRGB("txt",kwhieditstyle[0]);
  697. setdivstyle(["txt"]);
  698. }
  699. if (e.target.id == "btnbkgreset"){
  700. if (set == "new"){
  701. kwhieditstyle[1] = "rgb(255,255,0)";
  702. } else {
  703. kwhieditstyle[1] = hlobj[set].backcolor;
  704. }
  705. populateRGB("bkg",kwhieditstyle[1]);
  706. setdivstyle(["bkg"]);
  707. }
  708. e.target.blur();
  709. }
  710. function populateRGB(prop,stylestring){
  711. var rgbvals = stylestring.substr(stylestring.indexOf("(")+1);
  712. rgbvals = rgbvals.substr(0,rgbvals.length-1).split(",");
  713. document.getElementById(prop+"r").value = parseInt(rgbvals[0]);
  714. document.getElementById(prop+"g").value = parseInt(rgbvals[1]);
  715. document.getElementById(prop+"b").value = parseInt(rgbvals[2]);
  716. }
  717. function updatestyle(e){
  718. // validate value and apply change
  719. if (e.target.id.indexOf("txt") == 0 || e.target.id.indexOf("bkg") == 0){
  720. if (isNaN(e.target.value)){
  721. alert("Please only use values between 0 and 255");
  722. return;
  723. }
  724. if (parseInt(e.target.value) != e.target.value){
  725. e.target.value = parseInt(e.target.value);
  726. }
  727. if (e.target.value < 0){
  728. e.target.value = 0;
  729. }
  730. if (e.target.value > 255){
  731. e.target.value = 255;
  732. }
  733. if (e.target.id.indexOf("txt") == 0) setdivstyle(["txt"]);
  734. if (e.target.id.indexOf("bkg") == 0) setdivstyle(["bkg"]);
  735. } else {
  736. if (e.target.id == "kwhicustom") return;
  737. console.log("updatestyle on "+e.target.id);
  738. }
  739. }
  740. function setdivstyle(props){
  741. for (var i=0; i<props.length; i++){
  742. switch (props[i]){
  743. case "txt":
  744. kwhieditstyle[0] = "rgb(" + document.getElementById("txtr").value + "," +
  745. document.getElementById("txtg").value + "," + document.getElementById("txtb").value + ")";
  746. break;
  747. case "bkg":
  748. kwhieditstyle[1] = "rgb(" + document.getElementById("bkgr").value + "," +
  749. document.getElementById("bkgg").value + "," + document.getElementById("bkgb").value + ")";
  750. break;
  751. default:
  752. console.log("default?");
  753. }
  754. }
  755. var rule = "#stylecontrols>p>span{";
  756. if (kwhieditstyle[0].length > 0) rule += "color:"+kwhieditstyle[0]+";";
  757. if (kwhieditstyle[1].length > 0) rule += "background-color:"+kwhieditstyle[1]+";";
  758. if (kwhieditstyle[2].length > 0) rule += "font-weight:"+kwhieditstyle[2]+";";
  759. if (kwhieditstyle[3].length > 0) rule += kwhieditstyle[3]+";";
  760. document.getElementById("kwhiedittemp").innerHTML = rule + "}";
  761. }
  762. function kwhifwchg(e){
  763. kwhieditstyle[2] = e.target.value;
  764. setdivstyle([]);
  765. }
  766. function kwhicustom(e){
  767. kwhieditstyle[3] = document.getElementById("kwhicustom").value;
  768. setdivstyle([]);
  769. }
  770. // Context menu options -- do not replace any existing menu!
  771. if (!document.body.hasAttribute("contextmenu") && "contextMenu" in document.documentElement){
  772. var cmenu = document.createElement("menu");
  773. cmenu.id = "THDcontext";
  774. cmenu.setAttribute("type", "context");
  775. cmenu.innerHTML = '<menu label="Text Highlighter">' +
  776. '<menuitem id="THDshowbar" label="Show bar"></menuitem>' +
  777. '<menuitem id="THDenableset" label="Enable matching set"></menuitem>' +
  778. '<menuitem id="THDdisableset" label="Disable this set"></menuitem>' +
  779. '<menuitem id="THDnewset" label="Add new set"></menuitem>' +
  780. '</menu>';
  781. document.body.appendChild(cmenu);
  782. document.getElementById("THDshowbar").addEventListener("click",editKW,false);
  783. document.getElementById("THDenableset").addEventListener("click",cmenuEnable,false);
  784. document.getElementById("THDdisableset").addEventListener("click",cmenuDisable,false);
  785. document.getElementById("THDnewset").addEventListener("click",cmenuNewset,false);
  786. // attach menu and create event for filtering
  787. document.body.setAttribute("contextmenu", "THDcontext");
  788. document.body.addEventListener("contextmenu",cmenuFilter,false);
  789. }
  790. function cmenuFilter(e){
  791. document.getElementById("THDenableset").setAttribute("disabled","disabled");
  792. document.getElementById("THDenableset").setAttribute("THDtext","");
  793. document.getElementById("THDdisableset").setAttribute("disabled","disabled");
  794. document.getElementById("THDdisableset").setAttribute("THDset","");
  795. var s = window.getSelection();
  796. if (s.isCollapsed) document.getElementById("THDnewset").setAttribute("THDtext","");
  797. else document.getElementById("THDnewset").setAttribute("THDtext",s.getRangeAt(0).toString().trim());
  798. if (e.target.hasAttribute('txhidy15')){
  799. document.getElementById("THDdisableset").removeAttribute("disabled");
  800. document.getElementById("THDdisableset").setAttribute("THDset",e.target.getAttribute('txhidy15'));
  801. } else {
  802. document.getElementById("THDdisableset").setAttribute("disabled","disabled");
  803. if (!s.isCollapsed){
  804. document.getElementById("THDenableset").removeAttribute("disabled");
  805. document.getElementById("THDenableset").setAttribute("THDtext",s.getRangeAt(0).toString().trim());
  806. }
  807. }
  808. }
  809. function cmenuEnable(e){
  810. var kw = e.target.getAttribute("THDtext").toLowerCase();
  811. var toggled = false;
  812. for (var j = 0; j < hlkeys.length; ++j){
  813. var hlset = hlkeys[j];
  814. var kwlist = "|" + hlobj[hlset].keywords.toLowerCase() + "|";
  815. if(kwlist.indexOf("|" + kw + "|") > -1){
  816. if (hlobj[hlset].enabled == "true") break; // already enabled
  817. kwhienabledisable(hlset,true);
  818. refreshSetList();
  819. toggled = true;
  820. break;
  821. }
  822. }
  823. if (toggled == false){
  824. if (document.getElementById("thdtopbar").style.display != "block") editKW();
  825. if (document.getElementById("thdtopdrop").style.display != "block") thdDropSetList();
  826. }
  827. }
  828. function cmenuDisable(e){
  829. kwhienabledisable(e.target.getAttribute("THDset"),false);
  830. refreshSetList();
  831. }
  832. function cmenuNewset(e){
  833. //TODO - if there's a selection, get it into the form
  834. kwhinewset(e,e.target.getAttribute("THDtext"));
  835. }
  836.  
  837. // TESTING ONLY
  838. function flushData(){
  839. GM_setValue("kwstyles", "");
  840. }
  841. GM_registerMenuCommand("TEST ONLY - flush keyword sets for Text Highlighter", flushData);
  842.  
  843. })(); // end of anonymous function