WebEraser

Erase parts of any webpage --annoyances, ads, images, etc., permanently with just, Ctrl + Left-Click.

当前为 2016-10-19 提交的版本,查看 最新版本

// ==UserScript==
// @name        WebEraser
// @version     1.2.1
// @namespace   sfswe
// @description Erase parts of any webpage --annoyances, ads, images, etc., permanently with just, Ctrl + Left-Click.
// @include     *
// @require     https://code.jquery.com/jquery-3.1.0.js
// @require     https://code.jquery.com/ui/1.12.0/jquery-ui.js
// @resource    jqueryUiCss    https://code.jquery.com/ui/1.12.0/themes/base/jquery-ui.css
// @resource    whiteCurtains  https://github.com/SloaneFox/imgstore/raw/master/whiteCurtains.jpg
// @icon        https://github.com/SloaneFox/imgstore/raw/master/WebEraserIcon.jpg
// @run-at      document-start
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getResourceURL
// ==/UserScript==


//
// History
//
// updated Oct  2016.  v1.2.1 Adapted for use also in Google Chrome/Chromium web browser.
// updated Sept 2016.  v.1.2  Added user option to turn on the monitoring for new nodes (node mutations).

if (!chromeInit()) $(main);

var iframe=window!=window.parent, border_width=6;
var host=window.document.location.host,
    pathname=window.document.location.pathname, webpage=host+pathname, website=host;
var erasedElems=getHidElems(), askedAlready, gelem, gelems, gpre_elem, whiteCurtains_res="whiteCurtains",
    bblinker, promptOpen, rbcl="sfswe-redborder", pbcl="sfswe-prevborder", tbcl="sfswe-transparentborder";
var tab="&emsp;&emsp;&emsp; &emsp; "; // tab=5spaces, emsp=4spaces, but HTML tab in a <pre> wider hence extra emsp's.
var curtain_icon=getValue("ownImageAddr","")||GM_getResourceURL(whiteCurtains_res), curtain_cnt=0;
var zaplists=new zaplist_composite(), overlay=false;
var config=getValue("config",{keepLayout:"checked",monitor:{}}); if (!config.monitor) config.monitor={};

Number.prototype.in=function(){for (i of Array.from(arguments)) if (this==i) return true;}; // Use brackets with a literal, eg, (2).in(3,4,2);
Number.prototype.inRange=function(min,max){ if (this >=min && this<=max) return true;}; // Ditto.
Number.prototype.withinRangeOf=function(range,target){ return this.inRange(target-range,target+range); }; // Ditto.
String.prototype.truncate=function(maxsize) { if (this.length<=maxsize+5) return this; else return this.slice(0,maxsize/2)+"..."+webpage.slice(-maxsize/2); }; //sumarize with ellipsis
//thousand's comma, call Number.toLocaleString()
if (iframe) console.log=x=>null;       //logger(); // Logs from doc start.

document.addEventListener("scroll", function(e){ if (!overlay) return; e.preventDefault();e.stopPropagation();e.stopImmediatePropagation();},true);

function main() {
    //timer();
    init_jquery();
    inner_eraseElements();
    //$(window).click(handleClick);
    window.addEventListener("click",handleClick,true);
    GM_addStyle( jqueryui_dialog_css         //GM_getResourceText ("jqueryUiCss")
		+".sfswe-prevborder { border-color:transparent !important;border-width:"+border_width+"px !important;border-style:double !important; }"
		+".sfswe-transparentborder  { border-color:transparent !important;border-width:"+border_width+"px !important;border-style:double !important; }"
		+".sfswe-redborder { border-color:red !important; border-width:"+border_width+"px !important;border-style:double !important; }"
	       ); // A later defined rule has precedence when both rules in effect.
    //setTimeout(inner_eraseElements,1500);
    GM_registerMenuCommand("Erase Web Elements ["+(erasedElems?"some erased":"none erased")+"]", eraseElementsCmd,"","", "E");
    setTimeout(reattachTornCurtains,2000);
    gelems=$();
} //main()
		
function keypressHandler(event) { try{ //while prompt is open.
    //console.log("keypressHandler",event.key);
    var ip=$("#sfswe-seledel:enabled");
    if (ip.length) { //live typing of selector.
	setTimeout(ip=>{
	    var cval=ip.val();
	    if ($(cval).length) { // may unwind.
		highlightElement(0,"off",null,true);
		highlightElement($(cval),null,null,true); }
	},500,ip);
    } else  { // widen/narrow
	switch(event.key) {
	case "w": widen(); break;
	case "n": narrow(); break; 
	default: return; } 
	return false;
    }
} catch(e) {console.error("An key handler error:"+e+" "+e.lineNumber);} };

function handleClick(e,iframe_click) { try {
    if (e.shiftKey || e.altKey || e.metaKey) return;
    if (e.ctrlKey) { // cut item, delete element
	if (e.preventDefault) {e.preventDefault();   e.stopPropagation();}
	if (!iframe_click) {
	    var seltext_len=window.getSelection().toString().length;
	    window.status="webEraser, Ctrl-Click, on HTML element:"+e.target.tagName+" "+seltext_len+" "+iframe;
	    console.log(window.status,e.target);
	    if (seltext_len != 0) return;
	    if (e.target.blur) e.target.blur();
	    if (iframe)  {
		window.parent.postMessage("sfswe-iframe-click","*"); // msg,origin
		return false;
	    }
	}
	var permrm, target=e.target;
	while (/HTMLUnknownElement/.test(target.toString())) target=target.parentNode; //Avoid non HTML tags.
	if ($(target).is(".WebEraserCurtain")) {
	    let reply=confirm("This will completely remove selected item, continue?");
	    if (reply!=null) openCurtains("zap",$(target).siblings("img").addBack());
	}
	else if (!askedAlready) checkIfPermanentRemoval(target,function(permrm){
	    if (permrm==false)      { askedAlready=true;alert("You hit TEMP, ctrl-click from now until page is reloaded will merely remove elements from the page temporarily. ");}
	    if (permrm!=undefined)   inner_eraseElements("from Click");             //undefined==>escape (cancel)
	});
   	else target.style.setProperty("display","none","important");
	return false;
    } // endif e.ctrlKey
} catch(e) { console.error("Click handling error:"+e+" "+e.lineNumber); } };  //handleClick()

window.addEventListener('message', function(e){ //reads postMessage().
    if (e.data!="sfswe-iframe-click") return;
    var iframeEl=$("iframe").filter(function(){ return this.contentWindow==e.source; });
    handleClick({target:iframeEl[0],ctrlKey:true},"iframe_click");
}, false);

function sprompt(pretext,initval,cb,cancelbtnText="Cancel",okbtnText="OK") { // "Cancel" returns null reply, empty "OK" returns "" reply, Escape key returns undefined reply.  undefined==null is true. but not for ""
    var input_tag, input_style="width:80%;font-size:small;";
    if (initval!==undefined) input_tag=initval.length<50 ? "input" : (input_style="width:95%;height:100px;","textarea");
    var content=$("<div class=sfswe-content tabindex=2 style='outline:none;white-space:pre-wrap;'><div >"+pretext+"</div>"
		  +(initval!==undefined ? "<"+input_tag+" spellcheck='false' style='"+input_style+"'  tabindex='1'></"+input_tag+">":"")+"</div>");
    content.find("input:not(:checkbox),textarea").val(initval);
    var sp1=$(document).scrollTop();
    var dfunc=content.dialog.bind(content);
    var dialog=content.dialog({
	modal: true, width:"60%", // position: { my: "center", at: "center center-25%", of: window }, // Greater percent further to top.
	buttons: {
	    [cancelbtnText]: function() { cb(null, $(this).find("input,textarea").val()); dfunc("close");},
	    [okbtnText]: function() {  cb($(this).find("input,textarea").val()||""); dfunc("close"); }
	},
	close: function(e) { dialog.off("keydown"); $(document).scrollTop(sp1); if (e.key=="Escape") cb(undefined);dfunc("destroy");} }).parent();
    dialog.wrap("<div class=sfswe-sprompt></div>"); // allows css rules to exclude other jqueryUi css on webpage from own settings, a
    dialog.keydown(function(e){	if (e.key == "Enter" && !/textarea/i.test(e.target.tagName))    $("button:contains("+okbtnText+")",this).click();    });
    dialog.css({"z-index":2147483647, position:"fixed", top: "50px"});
    dialog.find(".ui-dialog-titlebar").remove(); // No img in css for close 'x' at top right so remove.  Title bar not in normal confirm anyhow.
    dialog.draggable("option","handle", ".ui-dialog-buttonpane"); //
    dialog.resizable();
    setTimeout(function(){content.focus();},100);
    return dialog; //.ui-dialog
}
function sconfirm(msg,cb,cancelbtnText,okbtnText) { return sprompt(msg,undefined,cb,cancelbtnText,okbtnText); }

function checkIfPermanentRemoval(target,cb) {   // called from click handler.
    var parent=target.parentNode, index=0;
    var msg="Permanently erase selected element(s) &mdash; now seen on page with a red border that blinks?"
	+"\nUse 'w' and 'n' keys freely, to widen and narrow your selection."
	+"\nEscape quits, Enter OKs.  Use the GM menu <a href='#abc"+Math.random().toString(36)+"'>Erase Web Elements</a> to edit internal code."
    	+"\nHit Temp button below for ctrl-click to erase element(s) temporarily and inhibit this prompting until reload."
	+"\n\nInternal code for selected<span id=fsfpe-tagel></span>element is:<br>"
	+"<div style='display:inline-block; position:relative;width:100%'><input disabled id=sfswe-seledel style='width:80%;margin:10px;'>"
	+"<div style='position:absolute; left:0; right:0; top:0; bottom:0;'></div></div>";
    $(document).keypress(keypressHandler);
    var dialog=sconfirm(msg,function(reply) { // null for cancel button, undefined for escape, otherwise, "" or string.
	$(document).off('keypress');
	$(":data(pewiden-trace)").data("pewiden-trace",""); // remove trace
	if (reply==="") reply=$("#sfswe-seledel").val().trim(); // reply=="" for ok (ok for confirm of seledel text.)
	if (reply===undefined) { highlightElement(0,"off","restore"); cb(undefined); return; } //undefined for Escape.
	if (reply)  {
	    sconfirm("Click 'Site' to erase from any page visited on this website, "+website
		     +",\n\nClick 'Page' button to erase selected elements from webpage, "+webpage
		     +".\n\nInternal code for Element:\n\t"+reply,function(reply2, iptxt){
		if (reply2===null) addToHidElements(reply);    // btn1 -> null, btn2 -> "<string>" null==undefined
		else if (reply2!=undefined) addToHidElements(reply+" site");     
		highlightElement(0,"off","restore");
		if (reply2!=undefined && $("#sfswe-checkbox6:checked").length) zaplists.add(reply);
		cb(true);
	    },"Page","Site");
	    return;
	}// if reply
	highlightElement(0,"off","restore");	cb(false);
    },"Temp","OK"); //confirm(msg);
    dialog.find(".ui-dialog-buttonpane").prepend("<input id=sfswe-checkbox6 type=checkbox style='margin-left:3px;'><label style='vertical-align: text-bottom;margin-left:7px;'>Completely delete element.</label>");
    dialog.find("a").click(eraseElementsCmd);
    $("#sfswe-seledel").next().click(e=>{
	var that=$(e.target).prev(); //, ip=$("<input style='width:80%;' value='"+that.val().trim()+"'>");
	that[0].disabled=false;
	that.next().css("display","none");
	that[0].setSelectionRange(999,999);
	that.focus();
	that.blur(e=>{e.target.disabled=true;$(e.target).next().css("display",""); });
    });
    highlightElement(target);
    setTimeout(function(){dialog[0].scrollIntoView();},100);
    
}//end checkIfPermanentRemoval()

function eraseElementsCmd() { try{
    // Called from GM script command menu and from click on prompt.
    // 
    var sitewide, erasedElems, page_erasedElems=[], site_erasedElems=[], no_sels;
    erasedElems=getHidElems("withSite");
    no_sels=!erasedElems?0:erasedElems.split(/,/).length;
    var dialog=sprompt(
	"To erase elements on this page at "+website
	    +", give a selector for it eg, 'DIV#main_column site', optional word 'site' erases the element at the entire website.  " 
	    +"For more than one selector use commas to separate"+(no_sels?", currently there is/are "+no_sels+" below":"")+".  To remove element hiding leave blank.  Reload if necessary."
	, erasedElems.replace(/,/g,", \n"),
	function(reply){ try{
	    if (reply == null) return; //cancel ==> null, undefined==> escape. (null is == to undefined!)
	    config={monitor:config.monitor}; delete config.monitor[website];
	    if ($("#sfswe-checkbox:checked").length)	config.noAnimation="checked";
	    if ($("#sfswe-checkbox2:checked").length)	config.keepLayout="checked";
	    if ($("#sfswe-checkbox3:checked").length) 	config.hideCurtains="checked";
	    if ($("#sfswe-checkbox5:checked").length)   config.monitor[website]="checked";
	    if ($("#sfswe-checkbox4:checked").length) {
		toggleCurtains();
		sprompt("Please enter http address of curtain image to be used.  If giving left and right images sperate with a space.  Leave empty to reset.  Accepts base64 image strings.","",function(reply2){
		    if (reply2!=null) {  setValue("ownImageAddr",reply2); curtain_icon=reply2||GM_getResourceURL(whiteCurtains_res);
		   $(".WebEraserCurtain").attr("src",curtain_icon);  }	    toggleCurtains(); });
	    } else {
		reply=reply.replace(/\s*,\s*/g,",").replace(/(?=[^,])\n(?=[^,])/g,",").split(/,/); // , newline->comma if none; if no comma all is put in [0]
		$(reply).each((i,str)=>{ //!!TBD check for dups.
		    if (str=="") return;
		    str=str.trim();
		    if (/\ssite$/.test(str)) site_erasedElems.push(str.replace(/\ssite$/,""));
		    else page_erasedElems.push(str);
		});
		try{$(reply);} catch(e){alert("Bad selector given."); throw(e);}
		setValue("config",config);
		setValue(website+":erasedElems",site_erasedElems.toString());
		setValue(webpage+":erasedElems",page_erasedElems.toString());
		zaplists.update();
		openCurtains();
		$(".Web-Eraser-ed").each(function(){
		    var that=$(this);
		    that.css({display: that.data("sfswe-display"), visibility: that.data("sfswe-visibility")});
		    that.removeClass("Web-Eraser-ed");
		});
		$(".WebEraserCurtains").remove();
		setTimeout(inner_eraseElements,1000,"fromPrompt"); //'cos openCurtains takes time
		//inner_eraseElements("fromPrompt");
	    }
	} catch(e){console.error("eraseElementsCmd,"+e+e.lineNumber+e.stack);}
    }); //dialog=sprompt(...)
    var keep_layout=config.keepLayout;
    dialog.find(".ui-dialog-buttonpane").prepend(
	"<div class=sfswe-ticks style='width:75%;float:left;font-size:10px;'>"
	    +"<input id=sfswe-checkbox2 type=checkbox style='float:left;"+(!keep_layout?" margin:0 3px;":"")+"' "+keep_layout+"><label>Preserve layout (in general).</label>"
	    +(keep_layout ? "<input id=sfswe-checkbox3 type=checkbox style='margin:0 3px 0 10px;height:12px' "+(config.hideCurtains||"")+"><label>Hide curtains (when preserving layout).</label>" : "")
	    +"<br><input id=sfswe-checkbox type=checkbox style='margin-left:3px;'"+(config.noAnimation||"")+"><label>Disable animation (in general).</label>"
	    +"<br><input id=sfswe-checkbox4 type=checkbox style='margin-left:3px;'><label>Set your own curtains' image.</label>"
	    +"<input id=sfswe-checkbox5 type=checkbox style='margin-left:55px;'"+(config.monitor[website]||"")+"><label>Monitor for new elements on this website.</label>"
	    +"</div>"
    );
    dialog.find("input:checkbox").css({"-moz-appearance":"none",height:12});
    dialog.find(".ui-dialog-content").attr("title","Current element matches at this webpage:\n"+bodymsg());
} catch(e){console.error("eraseElementsCmd,"+e+e.lineNumber+e.stack);}
} //eraseElementsCmd()

function inner_eraseElements(from) { try{
    //
    // Called at page load and when user sets selector(s) for erasure.
    // 1. Go through uncurtained elements for erasure and do curtainClose (or css display to none) on each.
    // 2. Class each as "Web-Eraser-ed" and backup css values that might get changed.
    // 3. If changes were made log details to console and to body attribute.
    //
    var erasedElems=getHidElems(), len=erasedElems.length, erasedElems_ar=erasedElems.split(/,/), count=0, nomatch=[];
    if (erasedElems_ar[0]=="") erasedElems_ar.shift(); //fix split's creation of array length one for empty string.
    var theErased=$(".Web-Eraser-ed"); theErased.removeClass("Web-Eraser-ed");
    erasedElems_ar.forEach(function(sel,i){
	erasedElems=$(sel); //Array.from(document.querySelectorAll(sel)); //$(sel), jQ cannot find duplicate ids.
	erasedElems.each(function() {
	    var eld=this,el=$(eld); // 40msecs per 'each' loop.
	    markForTheCurtains(el,eld,sel);
	    var no_anima=config.noAnimation, keep_layout=config.keepLayout;
	    if (no_anima && !keep_layout)  eld.style.setProperty("display","none","important");
	    else  if (el.css("display")!="none") closeCurtains(el, no_anima, measureForCurtains);
	    count++;
	}); //erasedElems.each()
	if (erasedElems.length==0) nomatch.push(sel);
    }); //forEach()
    theErased=$(".Web-Eraser-ed");
    if (len && config.monitor[website])  observeThings(); else observeThings("off");

    if (theErased.length==0)  { if (len) console.info("WebEraser msg: "+webpage.truncate(20)+" has no match for selectors:",getHidElems());
				return; }  ////////////////////
    if (nomatch.length) console.info("No match for some selectors:",nomatch,"at",webpage);
    var ieemsg="GM script WebEraser is using selectors to hide "+count+(count==1 ? " element that was":" elements that were")+" present on this webpage html: "+webpage
        +".\nSee GM menu command Erase Web Elements to check and edit selector list.  "
	+(config.keepLayout ? "" : "Keep layout is not ticked.")
	+(config.noAnimation ? "Animation is off." : "")
	+(config.hideCurtains ? "Hide curtains is ticked." : "");
    theErased.each(function(i){
	var that=$(this);
	var sel=that.attr("selmatch-sfswe");
	var onzaplist=zaplists.which(sel); // 10 msecs to here from prev in  closeCurtains() above.
	ieemsg+="\n"+(i+1)+":"+sel;
	ieemsg+=".\t\t"
	    +(onzaplist.zap ?
	      " => not displaying."
	      : onzaplist.keep_layout ? " => hidden."
	      : "" );
    });
    count=0;
    console.info(ieemsg);
    bodymsg(ieemsg.replace(/(.*\n){2}/,""),"init"); 
} catch(e){console.info("Error during inner_eraseElements(),"+e+", line:"+e.lineNumber+"\n\t\tStack:\n"+e.stack+" "+erasedElems_ar);}
}

function closeCurtains(el, noAnimKeepLayout, finishedCB=x=>x) { // called from inner_eraseElements()
    var that=closeCurtains; if (!that.final_curtain) that.final_curtain=0;
    var hide_curtains=config.hideCurtains, keep_layout=config.keepLayout;
    var old_curtained=el.prev().data("covered-el");
    if ( ! old_curtained || ! old_curtained.is(el))
	var [curtainRod,lrcurtains]=createCurtains(el,noAnimKeepLayout);
    else { var curtainRod=el.prev(), lrcurtains=curtainRod.children().children(); }
    var onzaplist=zaplists.which(el); // 20 msecs from prev
    if (noAnimKeepLayout) {
	lrcurtains.css({width:"50%"});
	if (onzaplist.zap) { curtainRod.css({display:"none"}); el[0].style.setProperty("display","none","important");} // "none" triggers monitor if on.
	else if (onzaplist.keep_layout||hide_curtains||curtainRod.hasClass("sfsweOverlay")){
	    curtainRod.css({visibility:"hidden",display:""});
	    el[0].style.setProperty("visibility","hidden","important");
	}
	measureForCurtains();
    }
    else { // Do animated curtain closing, then, perhaps, fade out.
	that.final_curtain++;
	manimate(lrcurtains,["width",50,"%"],2500,6,function(){ ///////////////////////Animation
	    el=$(this).closest(".WebEraserCurtains").data("covered-el");
	    if (!keep_layout || curtainRod.hasClass("sfsweOverlay")||onzaplist.zap) {
		el.add(curtainRod).delay(200).fadeOut(
		    500, function(){
			this.style.setProperty("display","none","important"); // triggers monitor if on.
			if (el[0]==this && --that.final_curtain==0) finishedCB();
		    });
	    }
	    else if (hide_curtains||onzaplist.keep_layout) {
		el.add(curtainRod).delay(200).fadeOut(
		    1000, function(){
			this.style.setProperty("visibility","hidden","important");
			this.style.setProperty("display",$(this).data("sfswe-display"),"important"); //triggers monitor.
			//curtainRod.css({visibility:"hidden",display:""});
			curtainRod.remove();
			if (el[0]==this && --that.final_curtain==0) finishedCB();
		    });
	    } else if (--that.final_curtain==0) finishedCB();
	}); //animate()
    }
} //closeCurtains()

function getSelectorWithNearestId(target,exclude_classes) {
    var sel, nearestNonNumericId=target.closest(":regexp(id,^\\D+$)").attr("id"), nnmi=nearestNonNumericId; //closest also checks target
    if (nnmi) nnmi=$("#"+nnmi).prop("tagName")+"#"+nnmi; //cos of jQ & multiple ids.
    if ($(nnmi).is(target)) sel=nnmi;
    else {
	sel=selector(target,$(nnmi),true,0,exclude_classes); //ok if nnmi is undefined id.
	if (!sel) sel=nnmi; //both target and $(nnmi) are same element. 
	else if(nnmi) sel=nnmi+sel;
    }
    return sel;
}

function getHidElems(withsite, justpels_ar){
    var els, pels=getValue(webpage+":erasedElems","").trim(), sels=getValue(website+":erasedElems","").trim();
    if (withsite && sels) {
	sels=sels.replace(/,/g," site,")+" site"; // see reverse of this in addToHidElements() and  eraseElementsCmd().
    }
    if (justpels_ar) return pels.split(",");
    return pels + (sels && pels ? "," : "") + sels;
}

function rmFromHidElements(str) {
    console.log("rmFromHidElements",str);
    addToHidElements(str,"rm");
}

function addToHidElements(str,rm) {  // called from checkIfPermanentRemoval() in handleClick on ctrl-click, "cut item" and 'x' del element.
    var page_erasedElems=getValue(webpage+":erasedElems","").trim(),
	site_erasedElems=getValue(website+":erasedElems","").trim(), sitewide;
    console.log("mathc ",page_erasedElems.match(str));
    if (!rm) {
	if (/\ssite$/.test(str)) { sitewide=true; str=str.replace(/\s+site$/,"");  }
	if (sitewide)  site_erasedElems += site_erasedElems ? ","+str : str;
	else  page_erasedElems += page_erasedElems ? ","+str : str;
    } else {
	if (page_erasedElems.includes(str)) 
	    page_erasedElems=$.map(page_erasedElems.split(/,/),el=>el.includes(str)?null:el.trim()).join(",");
	else if (site_erasedElems.includes(str)) 
	    site_erasedElems=$.map(site_erasedElems.split(/,/),el=>el.includes(str)?null:el.trim()).join(",");
    }
    console.log("page_erasedElems",page_erasedElems);
    setValue(website+":erasedElems",site_erasedElems);
    setValue(webpage+":erasedElems",page_erasedElems);
    zaplists.update();
}

//Blinks are double, one for selected elements, other is only when at top/bottom of narrow/widen chosen.
function highlightElement(elem, off, restore, merehl) { //also updates prompt with elem's selector.
    if (!off) { // on
	elem=$(elem);
	if (elem.length==0) return;
	gpre_elem=gelem;
	gelem=$(elem);
	if (!merehl) {
	    var seltext=$("#sfswe-seledel"); //sfs_pesel");
	    var newsel=getSelectorWithNearestId(gelem,tbcl+" "+rbcl+" Web-Eraser-ed");
	    gelems=$(newsel).not(gelem);
	    seltext.val(newsel); //+"<pre style='font-size:14.4px;'>\n\tHTML in pre</pre>");
	}
	$("#fsfpe-tagel").text(gelem.prop("tagName").toLowerCase());
	gelem.parents().addBack().addClass(tbcl);
	gelem.find(">:only-child").addClass(tbcl);
	gelem.add(gelems).toggleClass(rbcl);
	bblinker=setInterval(function(){ // normal "selected" blink.
	    if (gelems.length) gelems.toggleClass(rbcl);
	    else gelem.toggleClass(rbcl);    //.css({borderColor:"red",borderWidth:"9px",borderStyle:"double"});
	},1200);
	gelem.data("pewiden-trace","true"); //    if (!gelem.hasClass("pewiden-trace"))
	//gelem[0].scrollIntoView();
    }
    else {
	clearInterval(bblinker);
	gelem.removeClass(rbcl);
	if (restore) { $("."+tbcl).removeClass(tbcl); return; }
	return;
    }
}

function widen() { // .html() return &gt; encodings, .text() does not.  tab as @emsp must be set with html() not text()
    var seltext=$("#sfswe-seledel");
    if (/[:.][^>]+$/.test(seltext.val())) {
	var newsel=seltext.val().trim().replace(/[:.][^:.]+$/,"");
	seltext.val(newsel);
	gelems=$(newsel);
	gelems.addClass(rbcl);
	return;
    }
    if (gelems.length) { gelems.removeClass(rbcl);gelems=$();}
    var p=gelem.parent();
    if (p.is("html")) {
	blinkBorders(gelem); //blink double indicates top of hierarchy.
	return;
    }
    highlightElement(0,"off");
    highlightElement(p);
}
function narrow() {
    if (gelems.length) {
	widen();narrow(); // Follow trace back to el.
	return;
    }
    var trace=gelem.find(":data(pewiden-trace):first"); // trace left by highlightElement()
    if(trace.length==0) trace=gelem.find(">:only-child");
    if (trace.length==0) {
	blinkBorders(gelem);
	return;
    }
    highlightElement(0,"off");
    highlightElement(trace);
}

function blinkBorders(elem, interval=150, times=4) { // borders must already be set.
    times*=2;
    var cnt=0,i=setInterval(function(){
	cnt++;
	elem.toggleClass(rbcl);
	//!!
	elem.toggleClass(tbcl); 
	if (cnt==times) {clearInterval(i);elem.removeClass(rbcl);}// interference so rm class.
    },interval);
}

function init_jquery() {
    $.fn.reverse = Array.prototype.reverse; 
    $.fn.swap = function(to) {
	var a=this.eq(0), b=$(to).eq(0);
	var tmp = $('<span>').hide();
	a.before(tmp);
	b.before(a);
	tmp.replaceWith(b);
	return;
    };
    $.easing["stepper"] =  function (x, t, b, c, maxt) { // eg, see, console.log($.easing)  for other funcs.
	// var y=c*(t/=maxt)*t + b;
	// if (x<0.4) y=0.1;
	//console.log(x);
	//return y;
	return x;
    };
    $.extend($.expr[':'], {
	regexp: function(currentobj, i, params, d) { //filter type function.
	    params=params[3].split(/,/);       //eg, [ 'regexp', 'regexp', '', 'className,promo$' ]
	    var attr=params[0], re=params[1];  //eg, className, promo$
	    if (attr=="class") attr="className";
	    var val=currentobj[attr]+""||"";
	    if (attr=="className") return val.split(/\s/).some(function(cl){return cl.match(re);});
	    else return val.match(re);
	}});     //usage eg: $(“div:regexp(className,promo$)”); 				
    (function($){ 
	$.event.special.destroyed = {
	    remove: function(o) {
		if (o.handler) {
		    o.handler();
		}
	    }
	}; })(jQuery); //Usage: $("#anid").bind('destroyed', function() {// do stuff}) // only for is jQ  removed el.
} //init_jquery()

function selector(desc,anc,no_numerals,recursed,exclude_classes) { // descendent, ancestor, such that ancestor.find(ret.val) would return descendant.  If no ancestor given it gives it relative to body's parent node.   // See example usage in checkIfPermanentRemoval(). Numeraled classes/ids are excluded.
    anc=$(anc).eq(0); //apply only to first ancestor.
    if (anc.length==0) anc=$(document.body.parentNode); // !anc wouldnt work for a jq obj.
    desc=$(desc);
    if ( (desc.closest(anc).length==0 || desc.length!=1) && !recursed) {
	console.info("Too many elements or descendant may not related to ancestor:");
	console.info("Descendant is:"+selector(desc,0,0,true));
	console.info("Ancestor is:"+selector(anc,0,0,true)+".");
	return;
    }
    // Last element is highest in node tree for .parentsUntil();
    var sel=
	    desc.add(desc.parentsUntil(anc)) // up to but not including.
	    .reverse() // see above, needs: $.fn.reverse = Array.prototype.reverse; 
            .map(function() { // works from bottom up to ancestor, hence need for reverse().
		var t=$(this), tag=this.tagName.toLowerCase(), nth=t.prevAll(tag).length+1, id, cl, nthcl;
		id=this.id ? "#"+ this.id : "";
		cl=(this.className? "." + $.trim(this.className).replace(/\s/gi, ".") : "");
		if (exclude_classes) cl=cl.replace(RegExp(".("+exclude_classes.replace(/ /g,"|")+")","g"),"");
			if (no_numerals && /\d/.test(id)) id="";
		if (no_numerals && /\d/.test(cl)) cl="";
		if ( (cl && t.siblings(tag+cl).length==0)
		     || id
		     || t.siblings(tag).length==0)
		    nth=0;
		else if (cl && t.siblings(tag+cl).length!=0) {
		    cl+=":eq("+t.prevAll(tag+cl).length+")";   //jQuery only has :eq()
		    nth=0;
		}
		return tag+(nth?":nth-of-type("+nth+")":"")+id+cl; ////////////////////nth-of-type is One-indexed.
	    }) //map()
            .get()         //
	    .reverse()
            .join(">");
    if (desc.is(anc.find(">"+sel))) {
	if (anc.is(document.body.parentNode)) return "html>" + sel;
	return ">"+sel;
    } else {
	console.info("Selector result:\n\t"+sel+"  Not findable in ancestor, nor in body's parent.");
	if ($(sel).length) return sel; //Its the very top element, <HTML>.
    }
}
function markForTheCurtains(el,eld,sel) {
    el.css({overflow:"hidden"}).addClass("Web-Eraser-ed").attr("selmatch-sfswe",sel)
	.data({sfsweDisplay: eld.style.display, sfsweVisibility:eld.style.visibility, sfsweOverflow: eld.style.overflow}); // needed in case zero height element with floating contents. // To make it have dims, in case of zero height with sized contents.
}

function reattachTornCurtains(curtains=$(".WebEraserCurtains")) {
    var torn=false;
    curtains.each(function(){
	var that=$(this), el=that.data("covered-el");
	if (el.parent().length==0 || !el.hasClass("Web-Eraser-ed")) {
	    torn=true;
	    that.addClass("sfswe-delete","true");
	    //that.remove();
	}    });
    $(".sfswe-delete").remove();
    if (torn) inner_eraseElements();
}

function measureForCurtains(curtains=$(".WebEraserCurtains")) {
    curtains.each(function(){
	var that=$(this), el=that.data("covered-el"); //that.next(); // next is the covered elem.
	var w=el.outerWidth(), h=el.outerHeight(); // Includes padding & border, margin included if 'true' passed.  jQuery sets and unsets margin-left during this, provoking attrModifiedListener.
	if (!el.hasClass("Web-Eraser-ed")) {
	    el.addClass("Web-Eraser-ed");
	    el.css({overflow:"hidden"});
	}
	var off=moffset(el);
	that.css(off).css({height:h,width:w});
	zoomToShape(that.children());
    });
}

function bodymsg(str,init) {
    var b=$("body");
    var msg=b.attr("sfswe-message2")||"";
    if (msg.length>1300) msg=bodymsg.init;
    if (str) if (init) { b.attr("sfswe-message",str);bodymsg.init=str;}
             else { b.attr("sfswe-message2",msg+(msg?" ":"")+str);console.info("WebEraser Monitor: "+str);	 }
    return b.attr("sfswe-message");
}

function observeThings(off) {
    var that=arguments.callee; that.off=[];
    if (that.obs1) { that.obs1.disconnect(); that.obs2.disconnect(); }
    if (off) return;
    var sels=getHidElems(),
	nomonitor=set=>{ if (set==1) { that.off.push(true); that.obs1.takeRecords();} // jquery get causes set, hence inf.loop.
			 if (set==0) { that.off.pop(); that.obs1.takeRecords();} return that.off.slice(-1)[0]; };
    
    var parseCssText=str=>JSON.parse("{" + (str||"").replace(/[\w-]+(?=:)/g,'"$&"').replace(/:\s*(.+?)(?=;)/g,':"$1"').replace(/;/g,",").slice(0,-1) + "}");
    console.info("WebEraser msg: Monitoring for creation and appearance of elements to be erased:"+sels);
    obs1_connect(sels); var cnt=[0,0,0];
    
    function obs1_connect(selectors) {
	that.obs1=attrModifiedListener(document,selectors,["style"],function(mutrecs,b) {
	    if (nomonitor()) return;
	    nomonitor(1);

	    mutrecs.forEach( mutrec=>{
		var target=$(mutrec.target), t=mutrec.target,oldCssDisplay=mutrec.oldValue?mutrec.oldValue.match(/(display:)\s*(\w*);?/):null;
		var currentValue=target.css("display");   cnt[0]++;
		var oldval=parseCssText(mutrec.oldValue), currval=parseCssText(t.style.cssText);
		//console.log("Prev Changes:",csscmp(target.data("olderval"), oldval)+".  Compared to current:",csscmp(oldval,currval));
		target.data("olderval",oldval);
		if (currval.display=="none" && oldval.display!="none") {
		    bodymsg("change-nodisplay:"+target.attr("selmatch-sfswe"));
		    target.prev().css("display","none");
		    measureForCurtains();
		} else if (currval.display!="none" && oldval.display=="none" ) {
		    bodymsg("change-display:"+target.attr("selmatch-sfswe"));
		    target.prev().css("display","");
		    closeCurtains(target,true);
		}
		if (parseInt(currval.height||0)- parseInt(oldval.height||0)) {
		    bodymsg("change-height:"+nodeInfo(target));
		    measureForCurtains();
		}
	    }); //forEach
	    nomonitor(0);
	}); // obs1_connect()
    }
    that.obs2=nodeMutationListener(document,sels,function(foundArrayOfNodes, ancestorOfMutation,removed) {
	if (nomonitor()) return;
	nomonitor(1);
	foundArrayOfNodes.forEach(node=>{
	    var jQnode=$(node); cnt[1]++; 
	    if (!removed) { // node inserted.
		var foundsel;
		sels.split(/,/).forEach(sel=>{if($(sel).is(jQnode)) foundsel=sel;});
		bodymsg("new-node:"+foundsel);
		markForTheCurtains(jQnode,node,foundsel);
		setTimeout(x=>{nomonitor(1);closeCurtains(jQnode,false,measureForCurtains);nomonitor(0);},300);
	    } else { // node removed
		cnt[2]++; 
		//if(jQnode.attr("cc"))  {
		bodymsg("node-delete:"+jQnode.attr("selmatch-sfswe"));
		$(".WebEraserCurtains[cc='"+jQnode.attr("cc")+"']").remove(); //.filter(function(){return $(this).data()})
		measureForCurtains();
		//} 
	    }
	});//forEach
	nomonitor(0);
    },true);//nodeMutationListener()
}

function zoomToShape(z,slope=5) {
    var zoom_scale;
    z.each(function(i,z){
	z=$(z);	var sfsdiv=z.parent(), w=sfsdiv.outerWidth(), h=sfsdiv.outerHeight();
	zoom_scale=((x,s)=>10 + s*(1000-x)/90|0)(w,slope)/10; //(x=>(2900-2*x)/90|0)(w)/10;
	zoom_scale*=((x,s)=>5 + s*0.025*(x-20)|0)(h,2)/10;
	var img=z.children()[0];
	z.attr("zoomed-w-h-z",w+"-"+h+"-"+zoom_scale+"-slope-"+slope);
	zoom_scale = w>1000 ? [1,1] : [zoom_scale,1];
	if (GM_getValue("ownImageAddr","")) {
	    var nratio=img.naturalWidth/img.naturalHeight, maxRatio=10,
		iratio=w/2/h, asp_ratio=iratio/nratio;
	    zoom_scale=[1,Math.min(asp_ratio,maxRatio)];
	    if (asp_ratio<1) zoom_scale=[Math.min(1/asp_ratio,maxRatio),1];
	}
	z.css("transform","scale("+zoom_scale+")");
    });
    return zoom_scale;
}

function openCurtains(zap_or_keep="",curtains=$(".WebEraserCurtain")) { // called from ctrl-click with curtains, eraseElementsCmd() w/o curtains, and lrcurtains.click sets "keep"
    console.log("openCurtains",zap_or_keep);
    setTimeout(function() {
	manimate(curtains,["width",0,"%"],500,3,function() {
	    var that=$(this), erased_el=that.parent().parent().next();
	    var sel=erased_el.attr("selmatch-sfswe");
	    switch(zap_or_keep[0]) {
	    case "z": zaplists.add(sel);erased_el.css("display","none");measureForCurtains();break;
	    case "k": zaplists.add(sel,"keep");erased_el[0].style.setProperty("visibility","hidden","important");break;
	    case "t": that.parent().parent().css("display","none"); console.log(that.css("display"),that);break;
	    case "a": console.log("altzap");that.parent().parent().remove();rmFromHidElements(sel);break;
	    }
	    //erased_el.prev().css({display:"none"});
	});
    },1000);
}

function createCurtains(el, noAnimKeepLayout) {
    var h=el.outerHeight()|0,w=el.outerWidth()|0, area=h*w, iw=w/2, pos= moffset(el),    
	warea=window.innerHeight*window.innerWidth, csspos=el.css("position");
    // 9 msecs to here from function start.
    //console.log("closeCurtains h/w",h,w,"pos",pos);
    var lcurtain=$("<img class='WebEraserCurtain pwe-left' style='left:0;position:relative;' src="+curtain_icon.split(/\s+/)[0]+"></img>"),
	rcurtain=$("<img class='WebEraserCurtain pwe-right' style='right:0;position:relative;' src="+curtain_icon.split(/\s+/).slice(-1)+"></img>"),
	curtainRod=$("<sfswediv class=WebEraserCurtains cc="+(++curtain_cnt)+" style='z-index:2147483647; position:absolute; display:block; overflow:hidden;opacity:0.94;'></sfswediv>"), //inline is default here, 'd take full width of parent.
	zoomer=$("<div class='sfswe-zoomer' style=' height:100%; '></div>"),
	lrcurtains=lcurtain.add(rcurtain), sel=el.attr("selmatch-sfswe");
    el.attr("cc",curtain_cnt);
    zoomer.append(lcurtain,rcurtain);
    curtainRod.append(zoomer);
    lrcurtains.click(function({ctrlKey:ctrl,shiftKey:shift,altKey:alt}) {
	if (ctrl) return; if (shift) openCurtains("keep_layout",lrcurtains);
	else if (alt) openCurtains("azap",lrcurtains); else openCurtains("tzap",lrcurtains); return false;});
    curtainRod[0].title="Shift-Click here to persistently hide whilst keeping page layout.\nCtrl-click to persistently delete from layout.\nClick to reveal hidden area."
	+"\nSelector: "+sel;
    curtainRod.css({height:h,width:w}).css(pos).data({coveredEl:el,selmatchSfswe:sel});
    lrcurtains.css({ width: (!noAnimKeepLayout ? 0 : "50%" )}); // Initial width of each curtain.
    curtainRod.data("zoom",zoomToShape(zoomer));
    var scale=curtainRod.data("zoom")[0], calc=50-50/scale; // 50% is fully closed.
    lrcurtains.width(calc+"%").height("100%");
    lrcurtains.data("init-width",calc|0);
    var portions=area/warea*100|0;    //curtainRod.attr("init-calc",(calc|0)+" "+portions);
    if (portions>=60) { //>75% of window is covered.
	var visible_area;
	with (Math) {visible_area=min(w,window.innerWidth)*min(h,window.innerHeight);}
	if (visible_area>=warea*0.6) {
	    lcurtain.css({left:"10%"});
	    curtainRod.css({width:"80%",top:"10%"}).addClass("sfsweOverlay");
	    lrcurtains.css({height:h*0.8});
	    setTimeout(x=>$("html, body").css("overflow",(i,v) => v=="hidden" ? "auto": null).css("position",(i,v) => v=="fixed" ? "static": null),4000);
	    overlay=true;
	    //First event listener can stop prop to ones added later, ideally would be added at doc-start.
	    console.info("This is an Overlay (>2/3 covered, "+portions+"%): ",sel);}}
    el.before(curtainRod);
    return [curtainRod,lrcurtains];
}

function toggleCurtains() {
    var that=arguments.callee; 
    $(".WebEraserCurtains").each(function(){
	if (!that.xor)    {manimate($(".WebEraserCurtain",this),["width",50,"%"],2000,12);zoomToShape($(".sfswe-zoomer"));}
	else              manimate($(".WebEraserCurtain",this),["width", $(this).data("init-width"),"%"],4000,8);
    });
}

function zaplist_composite() { // composite pattern
    if (iframe) return;
    var zlists=[new zaplist(webpage),new zaplist(website),new zaplist(webpage,"kl"),new zaplist(website,"kl")];
    this.add=function(sel,keep_layout){ 
	zlists.forEach(function(el) { el.add(sel,keep_layout);});   };
    this.contains=function(el){  // may be a dom/jq object or a string selector.   
	return zlists.some(function(list) { return list.contains(el);}); };
    this.which=function(el) { 
	if (this.contains(el)) {
	    var has_keep_layout=zlists.map(v => v.contains(el)).includes("kl");
	    return {keep_layout:has_keep_layout,zap:!has_keep_layout};
	}
	return {keep_layout:false,zap:false};
    };
    this.update=function(sel){	zlists.forEach(function(el) { el.update();});  };
    this.toString=()=>"[object zaplist_composite]";
}

function zaplist(key,keytype) {
    var fullkey=key+":zaplist"+(keytype? ":"+keytype : "");
    var savelist=function() { setValue(fullkey,list); };
    var readlist=function() { return getValue(fullkey,[]); }; 
    var list=readlist(); 
    //console.log("zaplist created key:",key,", keytype",keytype,", list",list);
    
    this.add=function(str,kl) {
	if (!!kl != !!keytype) return;
	if(getValue(key+":erasedElems","").split(/,/).includes(str)) {
	    list.push(str);
	    savelist();
	}
    };
    this.contains=function(jqobjOrStr){
	if (list.length==0) return;
	if (jqobjOrStr.attr) jqobjOrStr=jqobjOrStr.attr("selmatch-sfswe");
	if (list.indexOf(jqobjOrStr) != -1)
	    return keytype||"zap";
    };
    this.rm=function(str) {
	var i=list.indexOf(str);
	if (i!=-1)   list.splice(i,1);
	savelist();
    };
    this.update=function() {
	if (list.length==0) return;
	var strs_ar=getHidElems().split(/,/);
	list=list.filter(function (lel) {
	    return strs_ar.includes(lel);
	});
	savelist();
    };
    this.toString=()=>"[object zaplist]";
}

function moffset(elem, eld=elem[0]) {
    if (elem.css("position").includes("fixed")) 
	return Object.assign(elem.position(),{position:"fixed"});
    var dominPar=elem.offsetParent()[0]; 
    return left_top(elem);

    function left_top(elem) {
	var {left,top}=elem.position(); // jQ sets & unset margintop during this for some reason, margins and floating els may disaffect calc!
	let margl=parseInt(elem.css('margin-left')), margt=parseInt(elem.css('margin-top'));
	//let bordl=parseInt(elem.css('border-left-width')), bordt=parseInt(elem.css('border-top-width'));
	var x = left + margl, y = top + margt;
	do {
	    //console.log("got y",y," from","top",top,"margt",margt,"bordt",bordt);
	    elem = elem.offsetParent();
	    if (elem.is(dominPar) || elem.is("html")) break;
	    let {left,top}=elem.position(); // jQ sets & unset margintop during this for some reason, margins and floating els may disaffect calc!
	    console.log(x,nodeInfo(elem),dominPar?nodeInfo(dominPar):"");
	    x += left; y += top;
	    console.log("got y",y," from +",top);
	} while (true)
	return { left: x, top: y };
    }
}	      

//
// MutationObserver functions.           Eg, var obs=nodeInsertedListener(document,"#results", myCBfunc);  function myCBfunc(foundArrayOfNodes, ancestorOfMutation);
// Requires jQuery.
// See https://www.w3.org/TR/dom/#mutationrecord for details of the object sent to the callback for each change.
// Four functions available here:
// Parameter, include_subnodes is to check when .innerHTML add subnodes that do not get included in normal mutation lists, these lower nodes are checked when parameeter is true.
// Return false from callback to ditch out.
 
function nodeInsertedListener(target, selector, callback, include_subnodes) {
    return nodeMutation(target,selector,callback,1, include_subnodes);
}
function nodeRemovedListener(target, selector, callback, include_subnodes) {
    return nodeMutation(target,selector,callback,2, include_subnodes);
}
function nodeMutationListener(target, selector, callback, include_subnodes) { //inserted or removed, callback's 3rd parameter is true if nodes were removed.
    return nodeMutation(target,selector,callback,3, include_subnodes);
}
function attrModifiedListener(target, selectors, attr, callback) { //attr is array or is not set.
    var attr_obs=new MutationObserver(attrObserver); 
    var config={ subtree:true, attributes:true, attributeOldValue:true};
    if (attr) config.attributeFilter=attr;
    attr_obs.observe(target, config);
    function attrObserver(mutations) {
	var results=mutations.filter(v=>{ return $(v.target).is(selectors);});
	if (results.length) callback(results);
	//console.time("attrObserver"+mutations.length);
    }
    return attr_obs;
}
//
// Internal functions:
function nodeMutation(target, selectors, callback, type, include_subnodes) { //type new ones, 1, removed, 2 or both, 3.
    var node_obs=new MutationObserver(mutantNodesObserver);
    var jQcollection=$(selectors), cnt=0;
    node_obs.observe(target, { subtree: true, childList: true } );
    return node_obs;
    
    function mutantNodesObserver(mutations, mu_obs) { 
	var sel_find, muts, node;
	for(var i=0; i<mutations.length; i++) {
	    if (type!=2) testNodes(mutations[i].addedNodes, mutations[i].target); // target is node whose children changed
	    if (type!=1) testNodes(mutations[i].removedNodes, mutations[i].target,"rmed"); // no longer in DOM.
	}
	function testNodes(nodes, ancestor, rmed) {
	    if (nodes.length==0) return;
	    var results=[], subresults=$();
	    for (var j=0,node; node=nodes[j], j<nodes.length;j++) {
		if (node.nodeType!=1) continue;
		if (rmed && jQcollection.is(node))  {results.push(node);jQcollection=jQcollection.not(node);}
		else if ($(node).is(selectors))     {results.push(node);jQcollection=jQcollection.add(node);}
		if (include_subnodes) //.innerHTML can add subnodes that do not get included in mutations, these lower nodes are checked here.
		    if (rmed) subresults=$(node).find(jQcollection);
  		    else subresults=$(node).find(selectors);
	    } //for
	    results=results.concat(subresults.toArray());
	    if (results.length) callback(results, ancestor,rmed);
	} //testNodes()
    };
} 
//
// End MutationObserver functions.  Usage example, var obs=nodeInsertedListener(document,"div.results", myCBfunc);  function myCBfunc(foundArrayOfNodes, ancestorOfMutation);
//

function manimate(objs,[css_attr,target,suffix],interval,noOf_subintervals,CB) { // CB is invoked once, at end.  $.animate max-ed out cpu for 30 secs or so.
    var len=objs.length,cnt=0,i;
    if (!len) return false;
    var maxi=objs.length-1, subinterval=interval/noOf_subintervals,
	init_int=parseInt(objs[0].style[css_attr]), // assume same initital position and same units/suffix for all objs.
	m=(target-init_int)/noOf_subintervals,
	linear=(v,i)=>init_int+m*(i+1),	// quad=(v,i)=>Math.min(target_int,init_int+(5/3)*Math.pow(i+1,2)-(5/3)*(i+1)),	// combo=(v,i)=>quad(v,i)/2+linear(v,i)/2,
	plotvals=new Uint32Array(noOf_subintervals).map(linear);
    subinterval+=random(-subinterval/5,subinterval/5), 
    i=setInterval(eppursimuove,subinterval,objs);
    function eppursimuove(that) {
	objs.css(css_attr,plotvals[cnt]+suffix);
	if (++cnt==noOf_subintervals) {     
	    clearInterval(i);  CB && CB.call(that);}}
}

function setValue(n,v) { return GM_setValue(n,JSON.stringify(v));}
function getValue(n,v) { try { return JSON.parse(GM_getValue(n,JSON.stringify(v)));}
			 catch(e){ var mv=GM_getValue(n,v);setValue(n,mv);return mv;}} // just for migration of verions from non JSON strings.
function random(min,max) {
    return Math.floor(Math.random() * ((max+1) - min)) + min;
}

function timer() { //console.time() and console.timeEnd() not working at mo.
    if (window!=window.parent || timer.log) return;
    var originalLogger = console.log;
    timer.log=originalLogger;
    console.log = function () {
	if (!timer.begin) {
	    timer.begin=Date.now();
	    originalLogger.call(this,">>>>Init timer "+location+":");
	}
	var args=Array.from(arguments);
	args.unshift((Date.now()-timer.begin)+"\t");
	originalLogger.apply(this, args);
    };
}

function logger() {
    $(document).dblclick(outputlogger);
    var originalLogger = console.log;
    logger.log=originalLogger;
    console.log = function () {
	if (!logger.this) logger.this=this;
	// Do your custom logging logic
	var argq=$(document).data("loggerq");
	var args=Array.from(arguments);
	if (!argq) argq=[];
	if (document.readyState!=logger.state) {
	    argq.push(document.readyState+":");
	    logger.state=document.readyState;
	}
	argq.push(args);
	$(document).data("loggerq",argq);
	
	args.push(document.readyState);
	originalLogger.apply(this, args);
    };
} //logger()

function csscmp(prevval, newval) { try{
    var that=arguments.callee;
    var covered={}, roll="";
    for (let i in prevval) {
	covered[i]=1;
	if (newval[i]===undefined) roll+="Removed: "+i+"="+prevval[i]+" ";
	else if (prevval[i]!=newval[i]) roll+="Changed: "+prevval[i]+" to: "+newval[i]+" ";
    }
    for (let i in newval) if (!covered[i]) roll+="Added: "+i+"="+newval[i]+" ";
    return roll||"Same";
}catch(e) {console.error("csscmp Error",e.lineNumber,e);}}

function nodeInfo(node) { return selector(node,node.parentNode,0,0,"Web-Eraser-ed").replace(/^html>body>/,""); }

function outputlogger() {
    var originalLogger=logger.log;
    var that=logger.this;
    
    var argq=$(document).data("loggerq");
    originalLogger.call(that,"===============Logger Output==========================");
    argq.forEach(function(v){
	originalLogger.call(that,v); //this changes in forEach in this case!
    }); // originalLogger.apply(this,argq);
    originalLogger.call(that,"===============End Logger Output=======================");
    return false;
};

function logStack(fileToo) { // deepest first.
    var res="", e=new Error;
    var s=e.stack.split("\n");
    if (fileToo) res="Stack of callers:\n\t\t"; //+s[1].split("@")[0]+"():\n\t\t"
    for (var i=1;i<s.length-1;i++)
	res+=s[i].split("@")[0]+"() "+s[i].split(":").slice(-2)+"\n";
    return !fileToo ? res : {Stack:s[0]+"\n"+res}; 
}

function Ppositions(el, incl_self,not_pos_break="") { 
    el=$(el); var roll="\n\n";
    var els=el.parents();
    if (incl_self) els=els.add(el).reverse();
	els.each(function(){
	var pos=$(this).css("position");
	roll+=this.tagName+" "+pos+"\n";
	if (! pos.includes(not_pos_break)) return false;
	//       /^((?!relative).)*$/   matches any string, or line w/o \n, not containing the str "relative"
    });
    return roll;
}

var jqueryui_dialog_css=(
    ""
	+".ui-dialog-content,.ui-dialog,.ui-dialog textarea { font-size: 12px; font-family: Arial,Helvetica,sans-serif; border: 1px solid #c5c5c5; "
	+"background:#fff; color:#333; padding:12px;margin:5px;} "
	+".ui-dialog-buttonpane {  background:whitesmoke; font-size: 10px; cursor:move; border: 1px solid #ddd; overflow:hidden; } "
	+".ui-dialog-buttonset { float:right; } "
	+".ui-widget-overlay { background: #aaaaaa none repeat scroll 0 0; opacity: 0.3;height: 100%; left: 0;position: fixed;  top: 0; width: 100%;}"
	+".ui-button,.ui-widget-content { text-align:left; color:#333; border: solid 1px #c5c5c5; padding: 6px 13px;margin: 4px 3px 4px 0;} "
	+".ui-corner-all,.ui-dialog-buttonpane {border-bottom-left-radius:30px;}"
	+".ui-button:hover { background-color: #ededed;color:#333; } "
	+".ui-button { background-color: #f6f6f6;}"
	+".ui-dialog {position:absolute;padding:3px;outline:none;}"
	+".ui-resizable-handle { position:absolute; cursor: url(data:image/svg+xml;base64,"
	+"iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAAAAACo4kLRAAAACXBIWXMAAAsTAAALEwEAmpwYAAABAUlEQVQY022RsWrCYACEvz9EK1HQCilYu+jWdNKlj9BNN1/Cp/IBFHTMExihdTCIm0sR26W6KNHkvw6lunjLjffdnXn7fG0/PzzeG0A/m+/Ve/REB3CL/laStn7RBTpOG0iTXgWg0ktSoO20DJDv5gBy3TxgWkQFAG+QSdnAAyhErMtuFSiN0nRUAqpuec2x2V/4YOr7fd2AH/ebR+zhbMOaCWJrF4GphfZ8sEhSNm3EVrJxY5pJkhGAkjtzNRxuyAGws2Ap0DKYWYDbQRek3e6K9A8/TNPhBf5mzbEBvDCTpCz0ADN25gJOkzPAeXICNL85spu8/N0B8LDafK0+ouQXfemVYVtdIewAAAAASUVORK5CYII="
	+") 10 10, row-resize; } .ui-resizable-sw {bottom:5px;left:5px;}"
	+".ui-resizable-w, .ui-resizable-e { width:10px;height:100%;top:-5px;} .ui-resizable-n, .ui-resizable-s { width:100%;height:10px;} .ui-resizable-n {top:-5px; } .ui-resizable-w {left:-5px; } .ui-resizable-e {right:-5px; }"
	+".ui-tooltip { font-size: 7px; }"
	+".sfswe-ticks * {font-size:11px;padding:0px;margin:2px;}"
    	+".sfswe-content :-moz-any(div,span,input) { font-size:13px;padding:6px;margin:4px 3px 4px 0;color:#333;}"
	+".sfswe-content :-moz-any(a,a:visited) { color:#333;text-decoration:underline; padding:0;margin:0;}"
	+".sfswe-content a:hover {opacity:0.5;}")
	.replace(/\.ui/g,".sfswe-sprompt .ui");

function chromeInit() {
    if (!this.GM_getValue || "Barychelidae"!=GM_getValue("arachnoidal","Barychelidae")){ //chromium
	console.info("WebEraser userscript in non GM_ mode for chrome/safari etc.");
	this.GM_getValue=function(a,b) { return JSON.parse(localStorage[a]||JSON.stringify(b)); };
	this.GM_setValue=function(a,b) { localStorage[a]=JSON.stringify(b);};
	this.GM_getResourceURL=function(url) { return "https://github.com/SloaneFox/imgstore/raw/master/whiteCurtains.jpg"; };
	this.GM_registerMenuCommand=x=>null;
	var xhr=new XMLHttpRequest();   
	xhr.onload = function() {
	    eval(this.response);
	    if (this.responseURL.endsWith("jquery-ui.js")) return;
	    $(main);
	    $(window).click(handleClick);
	    xhr.open('GET', "https://code.jquery.com/ui/1.12.0/jquery-ui.js");    xhr.send();
	};
	xhr.open('GET', "https://code.jquery.com/jquery-3.1.1.js");           xhr.send();
	return true;
    } else 
	return false;
}