Newer
Older
s4 / s4-main.js
@HIROSE Yuuji HIROSE Yuuji on 4 Dec 2022 37 KB Keep pjaxview in @all post
// 愛
(function (){
    var isOlderJS;	// Set in init();
    var hoverTextLines = 10;
    var hasTouchPad =
	(navigator.maxTouchPoints && navigator.maxTouchPoints >0);
    var myurl = document.URL,
	mypath = myurl.substring(myurl.lastIndexOf("/"));
    var art_m_list = [];
    var mathjax = false;
    let input_pdfsw = 'input[name="comppdf"]';
    if (mypath.match(/(.*)\/(.*)/)) {
	mypath = RegExp.$2;
	mypath = mypath.substring(0, mypath.lastIndexOf("?"));
	//alert("mypath="+mypath);
    }
    function escapeChars(old) {
	return old.replaceAll('&', '&')
	    .replaceAll('"', '"')
	    .replaceAll("<", '&lt;')
	    .replaceAll(">", '&gt;');
    }
    function collectElementsByAttr(elm, attr, val) {
	var e = document.getElementsByTagName(elm);
	if (!e) return null;
	var list = [];
	for (var i of e) {
	    if (i.getAttribute(attr) == val)
		list.push(i)
	}
	return list;
    }
    function nthChildOf(parent, n, elem) { // Return Nth child of type ELEM
	// N begins with 1
	var i=0;
	var le = elem.toLowerCase();
	for (var c of parent.childNodes) {
	    if (!c.tagName) continue;
	    if (c.tagName.toLowerCase() == le) {
		if (++i >= n) return c;
	    }
	}
	return null;
    }
    function insertRedirect(e) {
	var articleId, textarea = document.getElementById("text");
	var p =	e.target, checked = p.checked;
	while (p = p.parentNode)
	    if (p.nodeName.match(/^td$/i)) break;
	if (!p) return;
	while (p = p.nextSibling)
	    if (p.nodeName.match(/^td$/i)) break;
	if (!p) return;
	articleId = p.getAttribute("id");
	if (textarea && articleId) {
	    var tv = textarea.value, lines;
	    if (tv)
		lines = tv.split("\n");
	    else
		lines = [""];
	    var re = new RegExp("[, ]*#"+articleId+"(?![0-9])");
	    checked = (p.nodeName.match(/^input$/)
		       ? p.checked		// checkbox obeys its status
		       : !lines[0].match(re))	// a-elment toggles redirection
	    if (checked) {
		if (!lines[0].match(re)) {
		    var re2 = new RegExp(/>#[#0-9, ]+[0-9]/);
		    if (lines[0].match(re2))
			lines[0] = lines[0].replace(
			    re2, '$&, '+'#'+articleId);
		    else {
			if (lines[0] > "") lines[0] = " "+lines[0];
			lines[0] = ">#"+articleId+lines[0];
		    }
		}
	    } else {		// Remove #xxxxx
		if (lines[0].match(/^>#[0-9 ,]+#/)) // 2 or more #id's
		    lines[0] = lines[0].replace(
			new RegExp("^>#"+articleId+"[ ,]*"), ">").replace(
			new RegExp("[ ,]*#"+articleId), "");
		else {
		    lines[0] = lines[0].replace(
			new RegExp(">#"+articleId+"[ ,]*"), "");
		}
	    }
	    lines[0] = lines[0].replace(/^> *$/, '');
	    textarea.value = lines.join("\n");
	}
    }
    function registPjaxViewers(aHrefList) {
	let apos=art_m_list.length;
	for (let a of aHrefList) {
	    let href = a.getAttribute("href");
	    let localvar = apos;
	    let td = a.parentNode,
		tr = td.parentNode,
		id = td.id,
		text = td.textContent,
		author = tr.getElementsByTagName("a");
	    if (author) author = author[0].getAttribute("title");
	    if (href.match(/\?showattc\+article_m\+([0-9]+)$/)) {
		if (td.innerHTML.match(/読み取り不可/)) {
		    a.removeAttribute("href");
		    continue;
		}
		let url = RegExp.lastMatch;
		// console.log("pjaxView(e, "+href+", "+apos+")");
		a.addEventListener("click", function(e) {
		    // Shoud use closure local variable: localvar
		    pjaxView(e, href, localvar);
		}, false);
		apos++;
		art_m_list.push({
		    url: href, id: id, author: author, text: text
		});
	    }
	}
    }
    function registInsertDirect(aHrefList) {
	for (i of aHrefList)
	    if (i.getAttribute("href").match(/^#[0-9]+$/))
		if (RegExp.lastMatch == i.innerHTML)
		    i.addEventListener("click", insertRedirect, false)
    }
    function mathjaxUpdate(arg) {
	try {
	    if (MathJax && MathJax.typesetPromise) {
		MathJax.texReset();		// Reset Math counters
		MathJax.typesetPromise(arg); // MathJax v3
	    }
	} catch (err) {console.log(err);}
    }
    var ajaxSubmit;
    function replAddNews(newtable) {
	let newids = [], idlist=[];
	let getArticleID = function (td) {
	    return parseInt(td.parentNode.getElementsByTagName("td")[1].id);
	}
	for (let i of newtable.querySelectorAll("td.repl"))
	    newids.push(i);
	newids = newids.sort((a,b)=> {
	    return (getArticleID(a) - getArticleID(b));
	});
	for (i of newids)
	    idlist.push(getArticleID(i));
	console.log("IDList="+idlist.join());
	let cnt=0, ntr;
	let current = collectElementsByAttr("td", "class", "repl"),
	    ncur=0, n, icur=0, o, oid, nid, otr;
	current = document.querySelectorAll('td[class="repl"]');
	let last=current[current.length-1],
	    tbody = last.parentNode.parentNode;
	let addEventsToNewTr = function(tr) {
	    let td  = tr.getElementsByTagName("td"),
		td0 = td[0], td1 = td[1];
	    td0.classList.add("new");
	    registInsertDirect(td0.querySelectorAll("a[href]"));
	    registPjaxViewers(td1.querySelectorAll("a[href]"));
	}
	// Erase all "new article" flags before merging
	for (let i of document.querySelectorAll("td.new"))
	    i.classList.remove("new");
	// Now reconstruct articles with merge-sort like method
	outer: for (; ncur<newids.length; ncur++) {
	    n = newids[ncur];
	    if (!n.id) continue;
	    nid = parseInt(n.id);
	    if (nid<=0) continue;
	    ntr = n.parentNode;
	    for (; icur<current.length; icur++) {
		o = current[icur];
		otr = o.parentNode;
		oid = getArticleID(o);
		if (!oid || oid=="") continue;
		if (oid >= nid) {
		    addEventsToNewTr(ntr);
		    tbody.insertBefore(ntr, otr);
		    if (oid==nid) otr.remove();
		    cnt++;
		    continue outer;
		}
	    }
	    // Append absolutely new articles.
	    ntr = n.parentNode;
	    addEventsToNewTr(ntr)
	    tbody.appendChild(atMarkView(ntr));
	    ntr.classList.add("dissolving");
	    let localntr = ntr;
	    setTimeout(() => {
		localntr.classList.remove("dissolving");
		localntr.classList.add("emerging");
	    }, 100);
	    rewriteReplyHover(ntr);
	    cnt++;
	}
	mathjaxUpdate(newids);
	console.log("Update "+cnt+"rows");
	if (cnt>0 && ntr.scrollIntoView) {
	    let option = {behavior: "smooth"};
	    if (!isOlderJS) option.block = "center";
	    try {	// Scroll to last updated row
		ntr.scrollIntoView(option);
	    } catch (e1) {}
	}
	return cnt;
    }

    function warnFileSize(form) {
	let szmax = form.querySelector('input[name="filesize_max"]').value;
	if (!szmax || szmax=="") return;
	szmax = parseInt(szmax);
	if (szmax <= 0) return;
	// szmax = 10000
	let ng = "", rcval=false, fileexists=false,
	    pdfsw = form.querySelector(input_pdfsw),
	    pdfmsg = "Try compressing PDF?\nPDFを圧縮してみますか?\n" +
	    	     "(それでも収まらない場合もあります)";
	for (let f of form.querySelectorAll('input[type="file"]')) {
	    let thiserr = false;
	    for (let i of f.files) {
		fileexists = true;
		let fn = i.name, sz = i.size;
		console.log("max="+szmax+", fn="+fn+", sz="+sz);
		if (sz > szmax) {
		    if (fn.match(/\.pdf/i)
			&& sz < szmax*3	// XXX : x3 reasonable?
			&& (pdfsw || confirm(pdfmsg))) {
			if (!pdfsw) {
			    pdfsw = document.createElement("input");
			    pdfsw.name = "comppdf";
			    pdfsw.type = "hidden";
			    f.parentNode.insertBefore(pdfsw, f);
			    pdfsw.value = "yes";
			}
		    } else {
			thiserr = true;
			ng += ((ng>"" ? ", " : "")+fn)
		    }
		}
	    }
	    thiserr ? f.classList.add("warnbg") : f.classList.remove("warnbg");
	}
	if (ng>"") {
	    rcval = "File-size Limit Error: "+ng+"\n"+
		"Should be less than "+szmax+"bytes.\n"+
		szmax+"バイト未満にしてください"
	    alert(rcval);
	}
	if (form.text.value == "") {
	    let w;
	    if (fileexists)
		w = "Fill the text area\n" +
		"添付したファイルに関する説明を入れてください。";
	    else
		w = "Enter your comment!\n何か書き込んでね!";
	    alert(w);
	    rcval = (rcval || w);
	    form.text.classList.add("warnbg");
	    setTimeout(() => {form.text.classList.remove("warnbg");}, 2000)
	}
	return rcval;
    }
    function ajaxPost(e) {
	e.preventDefault();
	let rowid;
	if (!myurl.match(/replyblog\+([0-9]+)/)) return;
	rowid = RegExp.$1
	let myform = document.querySelector("form.replyblog");
	let data = new FormData(myform),
	    fetchtime = data.get("fetchtime");
	if (!fetchtime || fetchtime=="") return;
	///*XX*/fetchtime = "2020-06-14T00:00:00";data.set("fetchtime", fetchtime)

	ajaxSubmit = e.target;
	ajaxSubmit.back = ajaxSubmit.textContent;
	if (ajaxSubmit.id == "reload") {
	    ajaxSubmit.textContent = "更新中"
	    data.set("text", "")
	} else {
	    if (warnFileSize(myform)) return;
	    ajaxSubmit.textContent = "送信中";
	}
	ajaxSubmit.blur();
	ajaxSubmit.disabled = true;
	let act = mypath+"?blog_fetch+"+rowid+"+f:"+fetchtime;
	
	function respUpdate(tbody) {
	    ajaxSubmit.textContent = ajaxSubmit.back;
	    ajaxSubmit.disabled = false;
	    let div = document.createElement("div"), form, newform;
	    try {
		div.innerHTML = tbody;
		form = div.querySelector("form");
	    } catch (er) {
		alert("Cannot parse fetch data");
		return;
	    }
	    let update = replAddNews(form);
	    let dispelem = myform.querySelector("textarea").parentNode;
	    if (div.querySelector('input[name="user"]')) { // is login form
		dispInfoMomentary("Login Again Please", dispelem)
		return;
	    }
	    newform = new FormData(form);
	    if (data.get("text") > "") {	// Called by submit button
		myform.reset();
		let pdfsw = myform.querySelector(input_pdfsw);
		if (pdfsw) pdfsw.remove();
		// myform.text.value = '';
	    }
	    myform.fetchtime.value = newform.get("fetchtime");
	    myform.id.value = newform.get("id");
	    if (update && update > 0) {
		let s = update + " new article" +
		    (update>1 ? "s" : "") + " posted";
		dispInfoMomentary(s, dispelem);
	    }
	}
	fetch(act, {
	    method: "POST", body: data,
	    credentials: "include"	// For older firefox
	}).then((resp) => {
	    return resp.text();
	}).then((tbody) => {
	    respUpdate(tbody);
	})
    }
    function pjaxView(ev, url, mynum) {
	if (ev.ctrlKey||ev.shiftKey) return;
	ev.preventDefault();
	let box = document.createElement("div")
	box.setAttribute("class", "pjaxview");
	let p1 = document.createElement("p"),
	    bt = document.createElement("button"),
	    sl = document.createElement("button"),
	    sr = document.createElement("button"),
	    loading = document.createElement("span"),
	    info   = document.createElement("p");
	    info1  = document.createElement("span");
	    info2  = document.createElement("span");
	    iframe = document.createElement("iframe");
	var curpos = mynum;
	var historyBase = history.length;
	
	function _setPjaxCurposInfo() {
	    let len = art_m_list.length;
	    let cur = art_m_list[curpos]
	    info1.textContent = (1+curpos)+" of "+len+" article #"+cur.id+
		(cur.author ? " by "+cur.author : "") + ":";
	    info2.textContent = cur.text.trim();
	    info2.setAttribute("class", "border textdigest");
	}
	function _resetPjax() {
	    // All we can do surely is to back 1 page,
	    // because we cannot move to desirable entry of history list.
	    history.back();
	}
	function setSwipeAct(iframe) {
	    // We cannot use DOMContentLoaded nor iframe.contentWindow here.
	    // PDF.js does not construct contentWindow...?
	    iframe.addEventListener("load", () => {
		loading.classList.remove("loading");
		if (!hasTouchPad) return;
		let ifm = iframe.contentDocument;
		let startX, moveX, thresh = 100;
		ifm.addEventListener("touchstart", (e) => {
		    e.preventDefault();
		    startX = e.touches[0].pageX;
		}, false);
		ifm.addEventListener("touchmove", (e) => {
		    e.preventDefault();
		    moveX = e.touches[0].pageX;
		}, false);
		ifm.addEventListener("touchend", (e) => {
		    if (startX < moveX && startX + thresh < moveX) {
			switchTo(e, -1);
		    } else if (startX > moveX && startX - thresh > moveX) {
			switchTo(e, +1);
		    }
		}, false);
	    }, false);
	    
	}
	function switchTo(e, direction) {
	    e.preventDefault();
	    let len = art_m_list.length, cur, newpos, url;
	    newpos = (curpos+len+direction)%len;
	    if (curpos == newpos) return; // No need to switch to same one
	    curpos = newpos;
	    cur = art_m_list[curpos];
	    url = cur.url;
	    // We should remove iframe once to preserve history Object
	    // https://inthetechpit.com/2019/04/20/update-iframe-without-affecting-browser-history/
	    let parent = iframe.parentNode;
	    // alert("D = "+direction);
	    iframe.remove();
	    parent.appendChild(iframe);
	    try {
		loading.classList.add("loading");
		iframe.src = url;
		// iframe.contentDocument.location.replace(url);
		// location.replace cannot be used because PDF viewer.js
		// does not have iframe.contentDocument
	    } catch (err) {
		alert("Cannot load "+src+" : "+err.name);
	    }
	    _setPjaxCurposInfo();
	    setSwipeAct(iframe);
	}
	function switchToByKey(e) {
	    // alert("KEY="+e.key);
	    switch (e.key) {
	    case "ArrowLeft":
		switchTo(e, -1); break;
	    case "ArrowRight":
		switchTo(e, +1); break;
	    case "Escape":
		history.back();
	    }
	}
	// <div><p>
	// <button> << </button><button>Dismiss</button><button> >> </button>
	// </p><p><span> info1 </span> <span> info2 </span></p>
	// <iframe src="..."></iframe>
	// </div>
	// ==> [ << ][Dissmiss][ >> ]
	// ==> ## of ## article #xxx by AUTHOR
	sl.textContent = " << ";
	sr.textContent = " >> ";
	sl.addEventListener("click", (e) => {switchTo(e, -1);});
	sr.addEventListener("click", (e) => {switchTo(e, +1);});
	sl.setAttribute("title", "to="+(mynum-1));
	sr.setAttribute("title", "to="+(mynum+1));
	document.body.appendChild(box);
	bt.textContent = "Click to dismiss / もどる"+mynum;

	box.appendChild(p1);
	p1.appendChild(sl); p1.appendChild(bt); p1.appendChild(sr);
	{   // TEST: Normal mode
	    let only = document.createElement("button"),
		h = location.href;
	    only.textContent = ".oO□";
	    only.setAttribute("title", "Open in Normal Window");
	    only.addEventListener("click", function() {
		location.replace(iframe.src);
	    });
	    p1.appendChild(only);
	}
	p1.appendChild(loading);
	info.appendChild(info1); info.appendChild(info2);
	loading.textContent=" Loading...";
	loading.classList.add("hidden");
	loading.classList.add("loading");
	box.appendChild(info);
	iframe.src = url;

	box.addEventListener("keydown", switchToByKey);
	//box.addEventListener("click", (e) => {_resetPjax();});
	bt.addEventListener("click", (e) => {_resetPjax();});
	// dp.addEventListener("click", (e) => {_resetPjax();});
	info.addEventListener("click", (e) => {_resetPjax();});
	box.appendChild(iframe);

	setSwipeAct(iframe);

	_setPjaxCurposInfo();
	bt.focus();
	setTimeout(() => {box.classList.add("pjaxview2");}, 10);
	// Finally update history stack
	pjaxHistoryPush(box);
    }
    function pjaxHistoryPush(box) {
	if (history.pushState) {
	    let h = location.href.replace(/#.*/, '')+"#pjaxview";
	    history.pushState({url: h}, null, h);
	    window.addEventListener("popstate", (e) => {
		if (box) {
		    box.remove(); box = null;
		}
	    }, false);
	}
    }
    function reverseChecks() {
	var names = collectElementsByAttr("input", "name", "usel");
	for (let u of names) {
	    u.checked = !u.checked;
	}
    }
    function renumberOL(str, start) {
	var stra = str.split("\n");
	for (var i=1; i<stra.length; i++) {
	    if (stra[i].match(/^[1-9][0-9]*\. /)) {
		let orig=stra[i];
		stra[i] = (++start)+". "+RegExp.rightContext;
	    } else if (stra[i].match(/^  /)) {
		continue;
	    } else
		break;
	}
	return stra.join("\n");
    }
    function submitThisForm(e) {
	var input = e.target, ajaxpost = document.getElementById("c");
	for (var elm=input.parentNode; elm; elm = elm.parentNode) {
	    if (ajaxpost) {
		ajaxpost.click();
		return true;
	    } else if (elm.nodeName.match(/form/i)) {
		elm.submit();
		return true;
	    }
	}
	return false;
    }
    function helpMarkdownBS(e) {
	var area = e.target, pos = area.selectionStart, text = area.value;
	if (area.selectionStart != area.selectionEnd) return;
	if (pos<2) return;
	if (text.substr(pos-1, 2)=="\n\n") return;
	var bol  = text.lastIndexOf("\n", pos-1),
	    eol  = text.indexOf("\n", pos);
	if (bol<=0 || bol==eol) return;
	var thisline = text.substring(bol+1, eol==-1 ? text.length : eol);
	thisline = text.substring(bol+1, pos);
	if (thisline == "* ") {
	    area.setSelectionRange(pos-2, pos);
	} else if (thisline.match(/^[1-9][0-9]*\. $/)) {
	    area.setSelectionRange(pos-RegExp.lastMatch.length, pos);
	}
    }
    function helpMarkdownEnter(e) {
	if (e.keyCode == 13 && !e.shiftKey) {
	    if (e.ctrlKey && submitThisForm(e)) {
		e.preventDefault();
		return;
	    }
	    var area = e.target;
	    var pos  = area.selectionStart, text = area.value;
	    if (pos==0) return;
	    var last = text.lastIndexOf("\n", pos-1);
	    var rest = text.substring(pos), rest0=rest;
	    var line = last ? text.substring(last+1, pos) : text;
	    var next = rest.substring(rest.indexOf("\n"))||rest;
	    next=next.substring(1);
	    var tail = text.substring(pos-2, pos), br = (tail=="  ");
	    var add  = "", offset = 1;
	    if (line.startsWith("* ")) {
		add =  "* ";
		offset += add.length;
		if (br) {
		    add = "  " + "\n" + add;
		}
	    } else if (line.match(/^([1-9][0-9]*)\. /)) {
		var ln = parseInt(RegExp.$1), nn=ln+1,
		    len = RegExp.lastMatch.length;
		add = nn+". ";
		let toeol = text.substr(pos, text.indexOf("\n"));
		if (br) {
		    if (next.startsWith(add)) {
			add=" ".repeat(len);
			nn = ln;
		    } else {
			add = " ".repeat(len)+ "\n" + add;
			offset -= len+1;
		    }
		}
		if (next.match(/^[1-9][0-9]*\. /))
		    rest = renumberOL(rest, nn);
		offset += add.length;
	    } else if (line.match(/^\|( *).+\|/)) {
		add = "|" + RegExp.$1 + " |";
		offset += add.length-2;
	    } else {
		return;
	    }
	    e.preventDefault();
	    if (!document.execCommand("insertText", false, "\n"+add)) {
		//Firefox
		area.selectionEnd = area.value.length;
		area.setRangeText("\n"+add+rest);
		area.selectionEnd = null;
	    } else {
		area.selectionEnd = area.value.length;
		area.setSelectionRange(area.selectionStart, area.value.length);
		document.execCommand("insertText", false, rest);
		area.selectionEnd = null;
		area.focus();
	    }
	    area.selectionStart = pos+offset;
	    return;
	    if (document.execCommand("insertText", false, "\n"+add)) {
		//area.setSelectionRange(area.selectionStart, text.length);
		// alert("rest=["+rest+"], add=["+add+"]");
		alert(text.substring(pos, area.value.length));
		if (rest != rest0) {
		    area.setSelectionRange(pos, area.value.length);
		    return;
		    document.execCommand("delete");
		}
		document.execCommand("insertText", false, rest);
	    } else {
		// Firefox cannot use insertText in textarea...
		area.value = text.substring(0, pos) + "\n" + add + rest;
	    }
	    //area.setSelectionRange(pos+length(add));
	    area.selectionStart=area.selectionEnd = (pos + offset);
	    
	}
    }
    var helpParenPreview = 0;
    function helpMarkdownParen(e) {
	if (!mathjax) return;
	var area = e.target, pos = area.selectionStart, text = area.value;
	if (pos<2) return;
	if (text[pos-1] == "\\") {
	    let ins="(  \\)";
	    if (text[pos-2] == "\\") ins="(  \\\\)";
	    area.setRangeText(ins, pos, pos);
	    area.selectionStart = pos+2;
	    if (helpParenPreview++ < 1) {
		dispInfoMomentary("Preview formula by Meta-p\n"+
				  "Meta-p で数式プレビュー", e.target.parentNode);
	    }
	    e.preventDefault();
	}
    }
    function textInsert(area, string, pos1, pos2) {
	console.log("str="+string);
	area.setRangeText(string, pos1||area.selectionStart,
			  pos2||pos1||area.selectionStart);
	area.selectionStart += string.length;
    }
    function beginningOfLine(area, pos) {
	pos = pos||area.selectionStart;
	let b = area.value.lastIndexOf("\n", pos);
	if (pos>1 && area.value.charCodeAt(pos)==10)
	    b = area.value.lastIndexOf("\n", pos-1);;
	return b>=0 ? b : 0;
    }
    function isInBeginEnd(area, pos){
	pos = pos||area.selectionStart;
	let bol = beginningOfLine(area, pos);
	let thisline = area.value.substr(bol);
	console.log("curchar="+area.value.charCodeAt(pos));
	console.log("prechar="+area.value.charCodeAt(pos-1));
	console.log("bol="+bol+", thisline="+thisline);
	let match = thisline.search(/\\(begin|end){([A-Za-z]*)/), lm, be;
	if (match >= 0) {
	    lm = RegExp.lastMatch;
	    be = RegExp.$1;
	    return RegExp.$2
	}
	return null;
    }
    function helpMarkdownBrace(e) {
	if (!mathjax) return;
	var area = e.target, pos = area.selectionStart, text = area.value,
	    begin = "\\begin", end = "\\end";
	if (pos < end.length) return;
	if (text.substr(pos-end.length).startsWith(end)) {
	    let beg = text.lastIndexOf(begin, pos);
	    if (beg >= 0) {
		let env = text.substr(beg).search(/\\begin{(.*?)}/);
		if (env >= 0) {
		    textInsert(area, "{"+RegExp.$1+"}", pos);
		    e.preventDefault();
		}
	    }
	}
    }
    function helpMarkdownBraceClose(e) {
	if (!mathjax) return;
	let area = e.target, pos = area.selectionStart, text = area.value,
	    begin = "\\begin", end = "\\end";
	if (text.substr(pos).startsWith("}")) {
	    area.setRangeText("", pos, pos+1);
	    // e.preventDefault();
	}
	let inbegend = isInBeginEnd(area, pos);
	if (!inbegend) return;
	let nextendpos = text.substr(pos).indexOf("\\end{");
	let nextcurend = text.substr(pos).indexOf("\\end{"+inbegend+"}");
	if (nextcurend<0  || nextendpos!=nextcurend) {
	    area.setRangeText("}\n\n\\end{"+inbegend+"}", pos, pos);
	    area.selectionStart = pos+2;
	    e.preventDefault();
	}
	console.log(inbegend);
	
    }
    function helpMarkdownPreview(area) {
	if (!mathjax) {
	    alert("no"+e.target)
	    dispInfoMomentary("This board has no MathJax mode.\n"+
			      "この掲示板は数式モードOFFです。",
			      e.target.parentNode);
	    return;
	}
	let text = area.value;
	let preview = document.createElement("div");
	let bp = document.createElement("p");
	let btn = document.createElement("button");
	btn.innerText = "Click or ESC to Dissmiss / クリックかESCで戻る";
	bp.classList.add("c");
	preview.classList.add("pjaxview");
	preview.classList.add("pjaxview2");
	let pre = document.createElement("p");
	bp.appendChild(btn);
	preview.appendChild(bp);
	preview.appendChild(pre);
	pre.innerText = text;
	document.body.appendChild(preview);
	function dismiss(t) {
	    history.back();
	    preview.remove();
	    area.focus();
	}
	preview.addEventListener("click", dismiss, false);
	preview.addEventListener("keydown", dismiss, false);
	MathJax.typesetPromise([pre]);
	pjaxHistoryPush(preview);
	btn.focus();
    }
    function helpMarkdownAt(e) {
	var area = e.target, pos = area.selectionStart;
	if (pos == 0) {
	    area.value = "@all" + area.value;
	    area.selectionStart = area.selectionEnd = 4;
	    dispInfoMomentary("@all で全員に通知します", area.parentNode);
	    e.preventDefault();
	}
    }
    function helpMarkdown(e) {
	switch (e.key) {
	case "Backspace": helpMarkdownBS(e); break;
	case "Enter":  helpMarkdownEnter(e); break;
	case "(":  helpMarkdownParen(e); break;
	case "p":  if (e.metaKey) helpMarkdownPreview(e.target); break;
	case "{":  helpMarkdownBrace(e); break;
	case "}":  helpMarkdownBraceClose(e); break;
	case "@":  helpMarkdownAt(e); break;
	}
    }
    /* Init event listeners */
    function addFileInput() {
	var inpfile = collectElementsByAttr("input", "name", "image");
	if (!inpfile) return;
	var filled = true;
	var i, ih;
	for (i of inpfile) {
	    if (! i.value) filled=false;
	}
	if (filled) {
	    ih = i.parentNode.innerHTML;
	    if (ih) {
		var inpf = ih.substring(ih.indexOf("<input")),
		    newi = "<br>"+inpf.substring(0, inpf.indexOf(">")+1);
		i.insertAdjacentHTML("afterend", newi)
		i.nextSibling.nextSibling.addEventListener('change', () => {
		    // next==br next.next==input[type=file]
		    warnFileSize(document.forms[0]);
		});
	    }
	}
    }
    function initFileInput() { // Multiplies "input type=file"
	var el, morefile = document.getElementById("morefile");
	if (morefile) {
	    for (el of collectElementsByAttr("input", "name", "image")) {
		el.addEventListener("change", function(ev) {
		    if (ev.target.value > "" && ev.target.files.length == 1)
			morefile.style.visibility = "visible";
		    // No need to hide again, sure?
		});
	    }
	    morefile.addEventListener("click", addFileInput, null);
	}
	// When renaming, select basename part
	for (el of collectElementsByAttr("input", "class", "mv")) {
	    el.addEventListener("focus", function(ev) {
		var i = ev.target;
		if (i) {
		    i.setSelectionRange(0, i.value.lastIndexOf("."));
		}
	    });
	}
    }
    function initTextarea() {
	var te = collectElementsByAttr("textarea", "name", "text");
	if (!te || !te[0]) return;
	te[0].addEventListener("keydown", helpMarkdown, false);
    }
    function atMarkView(elem) {
	// Enclose "@all" with span
	for (i of elem.querySelectorAll("td.repl")) {
	    if (i.textContent.startsWith("@all")) {
		i.firstChild.nodeValue = i.firstChild.nodeValue.substring(4);
		i.insertAdjacentHTML(
		    'afterbegin', '<div class="atall">@all</div>'
		);
		i.classList.add("atall");
	    }
	}
	return elem;
    }
    var quizwarnVisible = false;
    function toggleAuthorVisibility(e) {
	// In QUIZ mode, click to quizwarn line toggles visibility of author
	e.stopPropagation();
	if (quizwarnVisible) {
	    for (let i of document.querySelectorAll("td.repatt")) {
		i.classList.remove("hideauthor");
	    }
	    quizwarnVisible = false;
	} else {
	    for (let i of document.querySelectorAll("td.repatt")) {
		i.classList.add("hideauthor");
	    }
	    quizwarnVisible = true;
	}
    }
    function downloadFile(filename, content, BOM) {
        let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
	let str = new Blob(BOM ? [bom, content] : [content],
			   {type: "text/csv"});
	var uri = URL.createObjectURL(str);
	let a   = document.createElement("a");
	a.download = (BOM ? "BOM-" : "")+filename;
	a.href = uri;
	document.body.appendChild(a);
	a.click();
	document.body.removeChild(a);
    }
    function getTextContentCSV_1(e) {
	let blogtbl = document.querySelector("table.blog_replies");
	let needBOM = e.ctrlKey;
	if (!blogtbl) return;
	let trw = blogtbl.querySelector("tr.warn"), a;
	if (trw && (a=trw.querySelector("th>a"))) {
	    if (a.title == "Show All") {
		if (window.confirm(`50件以下に表示制限されています。
取得し直しますか?
Cancelを押すとこのまま取得します。
Seen articles limited to 50 items.
Push OK to get all articles, Cancel to get only seen articles.`)) {
		    a.click();
		    return;
		}
	    }
	}
	if (navigator.userAgent.match(/Windows/)) {
	    if (!e.ctrlKey && !e.shiftKey && !window.confirm(`Excelで読ませるCSVの場合はBOMが必要です。
その場合は一度キャンセルして Ctrl キーを押しながらボタンクリックして下さい。
逆にExcel以外(GoogleスプレッドシートやLibreOfficeや他のツール)で読む場合はBOMをつけるとファイルの1行目の先頭にゴミのようなものが見える場合あるのでそのときは除去する必要があります。
今後もBOM不要の場合はShiftキーを押しながらクリックして下さい。
If you feed this CSV into Microsoft Excel, consider adding BOM sequence that can be prepended by pressing Control key with click.
In this case, click CSVget with ctrl key after Cancel this dialog.
If you never need BOM, press Shift key with click.`))
		return;
	}
	outcsv = []
	for (let row of blogtbl.querySelectorAll("tr[id]")) {
	    let tds = row.querySelectorAll("td"),
		a      = tds[0].querySelector("a.author"),
		author = a.title,
		name   = a.innerText,
		time   = tds[0].querySelector("span").title,
		id     = tds[1].id,
		body   = tds[1].textContent;
	    //console.log(`${author},${name},${time},#${id},${body}`);
	    outcsv.push({
		"author": author, "name": name, "time": time,
		"id": "#"+id, "body": body});
	}
	let line = new CSV(outcsv, {header:true}).encode(),
	    fn = myurl.replace(/.*\?/, "").replaceAll("+", "-")
	    .replace(/#.*/, "").replace("-n:all", "");
	downloadFile(fn+".csv", line, needBOM);
    }
    function getTextContentCSV(e) {
	if (!document.getElementById("csvminjs")) {
	    let csvmin = document.createElement("script");
	    csvmin.src="https://www.yatex.org/libcache/csv.min.js";
	    csvmin.id = "csvminjs";
	    // https://stackoverflow.com/questions/14521108/dynamically-load-js-inside-js
	    csvmin.addEventListener("load", ()=>{
		getTextContentCSV_1(e)}, 10);
	    document.querySelector("head").appendChild(csvmin);
	} else {
	    getTextContentCSV_1(e);
	}
    }
    function initBlogs() {
	// Auto-complete #xxxx
	let i, check = collectElementsByAttr("input", "name", "notifyto");
	if (check)
	    for (i of check) {
		i.addEventListener("click", insertRedirect, false);
	    }
	registInsertDirect(document.querySelectorAll("a[href]"));
	if (myurl.match(/replyblog\+[0-9]/)
	    && document.querySelector("td.repl")) {
	    // There's no need to provide ajax posting when
	    // no replies written to the blog.  Therefore we
	    // assign ajax post when td.repl exists.
	    for (i of document.querySelectorAll('input#c[value="送信"]')) {
		let b = document.createElement("button");
		b.textContent = "送信!";
		console.log("b="+b+", tc="+b.textContent);
		b.addEventListener("click", ajaxPost, false);
		// i.insertAdjacentElement('afterend', b);
		b.setAttribute("class", i.getAttribute("class"))
		b.setAttribute("title", i.getAttribute("title"))
		i.parentNode.replaceChild(b, i);
		b.id = i.id;
		// i.remove();
		i.classList.add("aux");
		i.value = "送信(予備)"
		b.parentNode.appendChild(i);
	    }
	    i = document.getElementById("reload");
	    if (i) i.addEventListener("click", ajaxPost, false);
	    // Add CSV download button
	    let td = document.querySelector("table.bloghead tr td");
	    if (td) {
		let btn = document.createElement("button");
		btn.innerText = "CSVget";
		btn.type = "button";
		btn.title = `見えている書き込みをCSVで取得します
全件表示されていることを確認してから利用して下さい。
Excelで利用する場合は Ctrl を押しながらクリックして下さい。
Get seen TEXT content as CSV.`;
		btn.addEventListener("click", getTextContentCSV, false);
		let artlink = td.querySelector('a[accesskey="f"]');
		let spacer = document.createElement("span");
		if (artlink) {
		    spacer.innerText = "|";
		    artlink.insertAdjacentElement('beforebegin', btn);
		    artlink.insertAdjacentElement('beforebegin', spacer);
		} else {
		    spacer.innerText = " ";
		    td.appendChild(spacer);
		    td.appendChild(btn);
		}
	    }
	}
	for (i of document.querySelectorAll('input[type="file"]')) {
	    i.addEventListener('change', (e) => {
		warnFileSize(document.forms[0]);
	    }, false)
	}
	if (i=document.getElementById("quizwarn")) {
	    i.addEventListener('click', toggleAuthorVisibility, false);
	}
	// Hack article_m links
	registPjaxViewers(document.querySelectorAll("a[href]"));
	atMarkView(document);
    }
    function initGrpAction() {
	var rev = document.getElementById("reverse");
	if (!rev) return;	// Is not grpAction page
	if (rev.tagName.match(/span/i)) {
	    rev.textContent = " 反転 ";
	    rev.addEventListener("click", reverseChecks, null);
	}
	var emailbtn = document.getElementById("email");
	emailbtn.addEventListener("click", function(ev){
	    // Enlarge box and Select user's checkbox
	    if (!ev.target.checked) return;
	    var x = collectElementsByAttr("div", "class", "foldtabs");
	    if (x && x[0] && x[0].style) {
		x[0].style.height = "10em";
	    }
	    let myuid = document.getElementById("myuid");
	    if (myuid) {
		let usel = collectElementsByAttr("input", "name", "usel");
		if (usel) {
		    for (u of usel) {
			if (u.value == myuid.value)
			    u.checked = true;
		    }
		}
	    }
	}, null);
	var teamsel = document.getElementById("selteam");
	if (teamsel) {
	    var usel, p, team;
	    // Select all members of the team
	    teamsel.addEventListener("change", function(ev) {
		var teamname = teamsel.value,
		    selected = new RegExp('(^| )'+teamname+"($|,)");
		usel = collectElementsByAttr("input", "name", "usel");
		if (!usel) return;
		for (u of usel) {
		    p = u.parentNode;		// should be label
		    if (!p) continue;
		    if (teamname == "TEAM") {	// Reset all checks
			u.checked = false;	// when "TEAM" is selected
		    } else {
			p = p.parentNode.parentNode;// should be tr
			team = nthChildOf(p, 5, "td")
			if (team && team.textContent
			    && team.textContent.match(selected)) {
			    u.checked = true;
			}
		    }
		}
	    }, null);
	}
    }
    function dispInfoMomentary(msg, elem) {
	// Momentarily display MSG in tooltip-baloon relative to ELEM element.
	let help = document.createElement("p");
	elem.style.position = 'relative';
	elem.style.overflow = 'visible';
	help.setAttribute("class", "info-tooltip");
	help.innerHTML = msg;
	elem.appendChild(help);
	setTimeout(() => {
	    help.classList.add("dissolving");
	    setTimeout(() => help.remove(), 3000);
	}, 1000);
    }
    function initGrphome() {
	console.log("initGrphome");
	// (1)Setup Frozen State Changing Button
	var ja = navigator.language.match(/ja/i);

	function toggleFrozen(e, rowid) {
	    let tgt = mypath+"?blog_setfrozen+"+rowid;
	    let td = e.target.parentNode;
	    let tr = td.parentNode;
	    fetch(tgt, {
		method: "POST",
		headers: {'Content-Type': 'text/html; charset=utf-8'},
		credentials: "include"
	    }).then(function(resp) {
		return resp.text();
	    }).then(function(tbody) {
		try {
		    var json = JSON.parse(tbody);
		} catch (e) {
		    return;
		}
		let state = json.state, newstate, info;
		if (json.alert) {
		    alert(json.alert)
		}
		if (state.match(/frozen/i)) {
		    newstate = "凍結";
		    info = ja ? newstate+"に設定しました" : 'Set Frozen';
		} else {
		    newstate = null;
		    info = ja ? '稼動に設定しました' : 'Set Running';
		}
		tr.setAttribute("class", newstate);
		dispInfoMomentary(info, td);
	    });
	}
	let btn = document.querySelectorAll("button.toggle-frozen");
	for (let b of btn) {
	    let rowid = null;
	    let td=b.parentNode, tr = td.parentNode, fr, ru;
	    ru = ja ? "動" : "Running";
	    fr = ja ? "凍" : "Frozen";
	    b.setAttribute('frozen-marker', fr);
	    b.setAttribute('running-marker', ru);
	    for (let a of tr.querySelectorAll("a[href]")) {
		if (a.getAttribute("href").match(/\?replyblog\+([0-9]+)/)) {
		    rowid = parseInt(RegExp.$1);
		    break;
		}
	    }
	    if (rowid && rowid>0) {
		b.addEventListener("click", function(e) {
		    if (!btn) return;
		    toggleFrozen(e, rowid);
		}, false);
		b.setAttribute("title", "稼動/凍結をその場で切り替えます\n\
Toggle Running/Frozen ("+rowid+")");
	    }
	}
	// (2)Setup Column Collapse Button
	// INCOMPLETE: Cannot restore original state, but it's enough...
	function toggleColmnWidth(th) {
	    let tbl = document.querySelector("table.dumpblogs");
	    let colname = th.textContent, newwidth;
	    if (th.style.width) {
		newwidth = null
		// https://developer.mozilla.org/ja/docs/Web/CSS/table-layout
		tbl.style.tableLayout = 'auto';
		tbl.style.width = null;
	    } else {
		newwidth = "2em";
		tbl.style.tableLayout = 'fixed';
		tbl.style.width = '100%';
	    }
	    th.style.width = newwidth;
	    th.style.overflow = "hidden";
	    for (let td of document.querySelectorAll("td."+colname)) {
		console.log(td.tagName);
		td.style.width = newwidth;
		console.log(td.style.width);
	    }
	}
	let row1 = document.querySelector("table.dumpblogs tr:first-child");
	if (row1) {
	    let heads = row1.querySelectorAll("th");
	    for (let h of heads) {
		h.addEventListener("click", function(e) {
		    toggleColmnWidth(h);
		}, false);
		h.setAttribute("title", "Click to shrink these columns");
	    }
	}
    }
    function initMath() {
	mathjax = window.MathJax||document.getElementById("mathjax");
	if (!mathjax) return;
	let ta = document.querySelector("textarea");
	if (!ta) return;
	let btn = document.createElement("button");
	btn.setAttribute("title", "\\( と \\) で数式利用\n"+
			 "\\[ と \\] で段組み数式モード\n"+
			 "便利なマクロ:\n"+
			 " \\boxed{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
			 " \\underline{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
			 "独自定義マクロ:\n"+
			 " \\warn{xxx}  注意喚起用色付き枠\n"+
			 " \\Warn{xxx}  大きな文字で注意喚起")
	btn.innerHTML = "MathJax<br>Preview";
	btn.addEventListener('click', (e) => {
	    e.preventDefault();
	    ta.focus();
	    helpMarkdownPreview(ta);
	});
	ta.parentNode.appendChild(btn);
    }
    function rewriteReplyHover(unit) {
	function getTextById(id) {
	    let repltd = document.getElementById(id);
	    if (repltd) {
		let txt = repltd.innerText,
		    authtd = repltd.parentNode.getElementsByTagName("td")[0],
		    author = authtd.querySelector("a.author").innerText,
		    digest = txt.split("\n").splice(0, hoverTextLines).join("\n");
		return escapeChars("[[ "+author+" ]]\n"+digest);
	    } else
		return "";
	}
	unit = unit||document;
	for (let td of unit.querySelectorAll("td.repl")) {
	    let firstC = td.firstChild;
	    // Direct replacing innerHTML breaks embedded DOM event handlers.
	    // So, we split td.repl into elements and replace the first
	    // textNode(nodeType==3) with hover-text embeded content.
	    if (firstC.nodeType==3 && firstC.nodeValue.startsWith(">#")) {
		let newline = firstC.nodeValue.indexOf("\n");
		let firstline;
		if (newline > 0) {
		    firstline = firstC.nodeValue.substring(0, 1+newline);
		    firstC.nodeValue = firstC.nodeValue.substring(1+newline);
		} else {
		    // Cannot be reached here, but leave this for robustness
		    firstline = firstC.nodeValue;
		    firstC.nodeValue = "";
		}
		td.insertAdjacentHTML(
		    'afterbegin',
		    escapeChars(firstline).replace(
			/#([0-9]+)/g,
			(match, start, whole) => {
			    let id = RegExp.$1
			    return '<a title="' + getTextById(id)
				+ '" href="' + match
				+ '">' + match + '</a>';
			}));
	    }
	}
    }
    function initReplyHover(unit) {
	// https://stackoverflow.com/questions/60154233/event-when-typesetting-is-done-mathjax-3
	if (mathjax && MathJax.startup)
	    MathJax.startup.promise.then(()=>rewriteReplyHover());
	else
	    rewriteReplyHover();
    }
    function init() {
	isOlderJS = !("insertAdjacentElement" in document.body);
	initGrpAction();
	initBlogs();
	initFileInput();
	initTextarea();
	initGrphome();
	initMath();
	initReplyHover();
    }
    document.addEventListener('DOMContentLoaded', init, null);
})();