WebEraser

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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;
}