changeset 898:411ce55c0dae

AJAX posting and PJAX file-viewer initially introduced.
author HIROSE Yuuji <yuuji@gentei.org>
date Sat, 02 Jan 2021 15:15:03 +0900
parents 3d437261edca
children a4ad4101064d
files examples/common/default/default.css s4-blog.sh s4-main.js scripts/s4-sns.case
diffstat 4 files changed, 449 insertions(+), 42 deletions(-) [+]
line wrap: on
line diff
--- a/examples/common/default/default.css	Thu Dec 31 10:31:13 2020 +0900
+++ b/examples/common/default/default.css	Sat Jan 02 15:15:03 2021 +0900
@@ -4,6 +4,9 @@
 body {background: #eff; margin: 2px; padding: 6px;}
 h2, h3, hr {clear: both;}
 *.warn {color: red;}
+*.warnbg {background: red;}
+*.hidden {visibility: hidden;}
+*.border {border: 1px solid #113;}
 div.topmenu {
     margin: 0; padding: 0; width: 100%; height: 2em;
 }
@@ -35,7 +38,9 @@
 table.td2r td:nth-child(2) {text-align: right;}
 table.td3r td:nth-child(3) {text-align: right;}
 table.td3rr td:nth-child(n+3) {text-align: right;}
-table.td3evw th:nth-child(2n+4) {background: white;}
+table.td3evw th:nth-child(2n+4), span.textdigest {
+    background: white;
+}
 table.thl th {text-align: left;}
 span#reverse {background: white; padding: 0 0 0 0.4ex; border: outset;}
 
@@ -165,6 +170,24 @@
 table.mini th, table.mini td {text-align: justify;}
 table.mini td, table.mini th {padding: 1px 0.5ex; min-width: 1em;}
 table.mini {margin-left: 1em; border-left: 2px solid #686;}
+div.pjaxview, div.pjaxview2 {
+    position: fixed; top: 1ex; left: 0; width: 9vw; height: 9vh;
+    background: #ffffee; border: 1px double navy; border-radius: 2em;
+    z-index: 7;
+}
+div.pjaxview2 {
+    width: 100vw; height: 95vw; transition: 0.5s;
+}
+div.pjaxview iframe {
+    width: 98%; height: 90%; object-fit: scale-down;
+}
+div.pjaxview p {
+    padding: 0.5ex; text-align: left; margin: 0 2em;
+    white-space: nowrap; overflow: hidden;
+}
+div.pjaxview p button { font-size: large; padding: 0 1em;}
+div.pjaxview p button { background: #eeeee0; }
+div.pjaxview p button:focus { background: #fffff8; }
 
 /* "visibility: collapse" not working on chromium browser */
 div.noprofimg tr.profimg {visibility: collapse; display: none;}
@@ -393,9 +416,13 @@
     border-radius: 1em; border: double 5px blue;
     min-width: 10em; right: 0; bottom: 0;
 }
-.dissolving {
-    opacity: 0; transition: 3.0s;
+.dissolving {opacity: 0; transition: 3.0s;}
+.emerging {opacity: 1; transition: 3s;}
+span.loading, input#c:disabled  {
+    visibility: visible; transform: rotateX(3600deg); transition: 30s;
+    display: inline-block;
 }
+span.loading {padding: 0 1em;}
 
 /*
  * PR Web
--- a/s4-blog.sh	Thu Dec 31 10:31:13 2020 +0900
+++ b/s4-blog.sh	Sat Jan 02 15:15:03 2021 +0900
@@ -178,6 +178,7 @@
       fi
     fi
   fi
+  err "blog_showentry  Entered: `gdate +%S.%03N` blogrowid=$rowid"
   blog_notify=`getvalbyid blog notify "$rowid"`
   blog_team=`blog_getteam "$rowid"`
   blog_mode=`getvalbyid blog mode "$rowid"`
@@ -201,6 +202,12 @@
 		THEN ''
 		ELSE 'Unreadable'
 		END"
+	  elif [ x"$blog_mode" = x"report-closed" ]; then
+	    F_UNREADABLE="CASE
+		WHEN author = '$user'
+		THEN ''
+		ELSE 'Unreadable'
+		END"
 	  else
 	    F_UNREADABLE="'Unreadable'"
 	  fi
@@ -226,6 +233,21 @@
   #err id=$id
   #err "select val from $ts where key='title' and id='$id';"
 
+  ## Parse control sequence
+  nlimit=$listartlimit
+  case "$control" in
+    n:[Aa][Ll][Ll])
+      unset nlimit ;;
+    n:*)
+      nlimit=${control##*:}
+      nlimit=${nlimit%%[!A-Z0-9a-z_]*}
+      ;;
+    f:[Aa][Ll][Ll])	;;
+    f:2???-??-??*)	# f:2020-12-27T08:02:43
+      fetch=${control#f:}
+      fetch_ajax=`echo "'$fetch'"|tr T ' '`
+  esac
+  err control=$control fetch_ajax=$fetch_ajax
   #(1)Display root article
   cat<<EOF
 <form class="replyblog" action="$myname?replyblog+${rowid}#bottom" method="POST" enctype="multipart/form-data">
@@ -271,18 +293,21 @@
 	EOF
   if test -s $midfile && IFS='|' read edit ctime hexhead blogtype < $midfile
   then
-    cat<<-EOF
+    if [ -z "$fetch_ajax" ]; then	# UUUUU
+
+      cat<<-EOF
 	<tr><td>${edit:+$href }$ctime $blogtype $href2${edit:+$href3} $href4 $href5</td></tr>
 	<tr class="preface${frozen_class:+ }$frozen_class">
 	 <td>`echo "$hexhead"|unhexize|htmlescape|hreflink|minitbl`</td></tr>
 	</table>
 	EOF
-    case "$blogtype" in
-      "クイズ"|"XXXX集計")
-	echo "${blogtype}モードは本人と管理者の書き込みのみが表示されます。"
-	;;
-    esac | html p 'class="warn"'
+      case "$blogtype" in
+	"クイズ"|"XXXX集計")
+	  echo "${blogtype}モードは本人と管理者の書き込みのみが表示されます。"
+	  ;;
+      esac | html p 'class="warn"'
 
+    fi			# UUUUU
     if [ x"$blogtype" = x"クイズ" -o x"$blogtype" = x"XXXX集計" ]; then
       if $isgroup; then
 	# Failsafe to query timeout
@@ -307,16 +332,7 @@
     echo "時間をおいて繋いでください(Please visit later)." | html p
     return
   fi
-  ## Parse control sequence
-  nlimit=$listartlimit
-  case "$control" in
-    n:[Aa][Ll][Ll])
-	unset nlimit ;;
-    n:*)
-	nlimit=${control##*:}
-	nlimit=${nlimit%%[!A-Z0-9a-z_]*}
-	;;
-  esac
+
   lkhome="<a href=\"$myname?home" lke='">'
   lkedit="<a href=\"$myname?editart"
   hlink="$myname?home" elink="$myname?editart"
@@ -392,7 +408,8 @@
       $cond_qz) a
   LEFT JOIN
      a_s s
-  ON a.id=s.id;
+  ON a.id=s.id
+  ${fetch_ajax:+WHERE s.TIME > $fetch_ajax};
 EOF
   if [ $? -ne 0 -a ! -s $midfile ]; then
     echo "時間をおいてください(Visit later please)." | html p
@@ -426,6 +443,7 @@
   else
     CAT=cat
   fi
+  err "blog_showentry  Started: `gdate +%S.%03N` ${fetch_ajax:+ajax}"
   # Start blog_replies table
   $CAT $midfile |
   while IFS='|' read id edit notify uid author uname icon aid \
@@ -487,6 +505,7 @@
 	usecache='' tsfile=$td/$id.stamp
 	for i in $imgids; do
 	  mrid=${i%%:*}; i=${i#*:}; sz=`size_h ${i%%:*}`
+	  _href="href=\"$catlink+$mrid\""
 	  fn=`echo "${i#*:}"|unhexize`
 	  fnb=$fn"(${sz})"
 	  case "$fn" in
@@ -508,7 +527,7 @@
 			   };}; then
 		usecache=1		# Set usecache flag on
 		cat<<-EOF
-		<a href="$catlink+$mrid"><img src="$outfile">
+		<__UNCLICKABLE__><a $_href><img src="$outfile">
 		$fnb</a>
 		EOF
 		# !!NOTE!! Create row stamp ONLY WHEN imgcache is active
@@ -522,18 +541,18 @@
 		  cat "$outfile" \
 		    | hexize \
 		    | sed -e 's/\(..\)/%\1/g' \
-	    		  -e "s|^|<a href=\"$catlink+$mrid\"><img src=\"data:image/$fmt,|" \
+	    		  -e "s|^|<__UNCLICKABLE__><a $_href><img src=\"data:image/$fmt,|" \
 			  -e "s|\$|\">$fnb</a>|"
 		  unset stampfile # img data stream is not suitable to cache
 		  echo $tm > $tsfile
 		else	# Failed to convert
 		  rm -f $outfile
-		  echo "<a href=\"$catlink+$mrid\">$fnb</a>"
+		  echo "<__UNCLICKABLE__><a $_href>$fnb</a>"
 		fi
 	      fi
 	      ;;
 	    *)
-	      echo "<__UNREADABLE__><a href=\"$catlink+$mrid\"><img src=\"$deficon\">$fnb</a>"
+	      echo "<__UNCLICKABLE__><a $_href><img src=\"$deficon\">$fnb</a>"
 	      ;;
 	  esac
 	done
@@ -548,10 +567,18 @@
       fi
       test -n "$stampfile" && date "+%F %T" > $stampfile
     fi
+    if [ -n "$fa" ]; then
+      replhref="s/a href=[^>]*>/a>/"
+    else
+      replhref=""
+    fi
     # Printing a cached row
     sed -e "/^<td class=/s/__NEWCLS__/$new${new:+ }/" \
 	-e "/^<td class=/s,__EDIT__,$editlink," \
 	-e "/^<__NOTIFY__>/s,,${notify:+$nt}," \
+	${replhref:+-e "/^<__UNCLICKABLE__>/$replhref"} \
+	${replhref:+-e "/^<__UNREADABLE__>/$replhref"} \
+	-e "/<__UNCLICKABLE__>/s///" \
 	-e "/<__UNREADABLE__>/s,,${fa:+$cannotread}," \
 	$cachefile
   done
@@ -589,10 +616,12 @@
 `cgi_file image "" "$file_accept title=\"$filehelp\" multiple"`
 </td></tr>
 </table>
-<input type="submit" value="送信" class="$blog_notify" title="$ntmode">
+<input type="hidden" name="fetchtime" value="`date +%FT%T`">
+<input type="hidden" name="filesize_max" value="$filesize_max">
+<input type="submit" id="c" value="送信" class="$blog_notify" title="$ntmode">
 <input type="reset" value="リセット"></div></div>
 EOF
-	)
+	  )
   cat<<-EOF
 	</table> <!-- end of s4-blog:blog_showentry() main table -->
 	<p class="update_link"><a
@@ -615,6 +644,7 @@
   [ -s $iconcleaner ] && query ".read '$iconcleaner'"
   # Record access log
   acclog blog $rowid
+  err "blog_showentry Finished: `gdate +%S.%03N` ${fetch_ajax:+ajax}"
 }
 
 lshandout() {
@@ -1522,6 +1552,13 @@
   err "blog_reply Finished: `gdate +%S.%03N` user=$user owner=[$owner] title=[$title]"
 }
 
+blog_fetch() {
+  contenttype "text/plain; charset=utf-8"; echo
+  err blog_fetch: blog "$@"
+  blog_reply "$@"
+  # blog_showentry blog "$@"
+}
+
 blog_reply_article() {		# Direct link to article in some blog
   arid=${1:-0}			# Already sanitized to digits
   brid=`query "SELECT rowid FROM blog WHERE \
--- a/s4-main.js	Thu Dec 31 10:31:13 2020 +0900
+++ b/s4-main.js	Sat Jan 02 15:15:03 2021 +0900
@@ -1,5 +1,16 @@
 // 愛
 (function (){
+    var isOlderJS;	// Set in init();
+    var hasTouchPad =
+	(navigator.maxTouchPoints && navigator.maxTouchPoints >0);
+    var myurl = document.URL,
+	mypath = myurl.substring(myurl.lastIndexOf("/"));
+    var art_m_list = [];
+    if (mypath.match(/(.*)\/(.*)/)) {
+	mypath = RegExp.$2;
+	mypath = mypath.substring(0, mypath.lastIndexOf("?"));
+	//alert("mypath="+mypath);
+    }
     function collectElementsByAttr(elm, attr, val) {
 	var e = document.getElementsByTagName(elm);
 	if (!e) return null;
@@ -67,6 +78,325 @@
 	    textarea.value = lines.join("\n");
 	}
     }
+    function registPjaxViewers(aHrefList) {
+	// if (isOlderJS) return;
+	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(/x読み取り不可/)) {
+		    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
+		});
+	    }
+	}
+    }
+    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;
+	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;
+	// 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;
+	    let ntr = n.parentNode;
+	    for (; icur<current.length; icur++) {
+		o = current[icur];
+		otr = o.parentNode;
+		oid = getArticleID(o);
+		if (!oid || oid=="") continue;
+		if (oid >= nid) {
+		    ntr.getElementsByTagName("td")[0].classList.add("new");
+		    tbody.insertBefore(ntr, otr);
+		    if (oid==nid) otr.remove();
+		    cnt++;
+		    continue outer;
+		}
+	    }
+	    // Append absolutely new articles.
+	    ntr = n.parentNode;
+	    ntr.getElementsByTagName("td")[0].classList.add("new");
+	    tbody.appendChild(ntr);
+	    registPjaxViewers(ntr.querySelectorAll("a[href]"));
+	    ntr.classList.add("dissolving");
+	    ntr.scrollIntoView({behavior: "smooth"});
+	    setTimeout(() => {
+		ntr.classList.remove("dissolving");
+		ntr.classList.add("emerging");
+	    }, 100);
+	    cnt++;
+	}
+	ajaxSubmit.value = ajaxSubmit.back;
+	ajaxSubmit.disabled = false;
+	console.log("Update "+cnt+"rows");
+	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;
+	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) {
+		    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");
+	if (warnFileSize(myform)) return;
+	ajaxSubmit = e.target;
+	ajaxSubmit.back = ajaxSubmit.value;
+	ajaxSubmit.value = "送信中";
+	ajaxSubmit.blur();
+	ajaxSubmit.disabled = true;
+	let data = new FormData(myform),
+	    fetchtime = data.get("fetchtime");
+	if (!fetchtime || fetchtime=="") return;
+	///*XX*/fetchtime = "2020-06-14T00:00:00";data.set("fetchtime", fetchtime)
+	let act = mypath+"?blog_fetch+"+rowid+"+f:"+fetchtime;
+	
+	function respUpdate(tbody) {
+	    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;
+	    newform = new FormData(form);
+	    myform.reset();
+	    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) {
+	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);
+	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;
+
+	document.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
+	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) {
@@ -75,7 +405,6 @@
     }
     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];
@@ -192,9 +521,9 @@
 	}
     }
     function helpMarkdown(e) {
-	switch (e.keyCode) {
-	case  8: helpMarkdownBS(e); break;
-	case 13: helpMarkdownEnter(e); break;
+	switch (e.key) {
+	case "Backspace": helpMarkdownBS(e); break;
+	case "Enter":  helpMarkdownEnter(e); break;
 	}
     }
     /* Init event listeners */
@@ -212,7 +541,10 @@
 		var inpf = ih.substring(ih.indexOf("<input")),
 		    newi = "<br>"+inpf.substring(0, inpf.indexOf(">")+1);
 		i.insertAdjacentHTML("afterend", newi)
-		// alert(newi);
+		i.nextSibling.nextSibling.addEventListener('change', () => {
+		    // next==br next.next==input[type=file]
+		    warnFileSize(document.forms[0]);
+		});
 	    }
 	}
     }
@@ -248,12 +580,28 @@
 	var check = collectElementsByAttr("input", "name", "notifyto");
 	if (check)
 	    for (let i of check) {
-		i.addEventListener("click", insertRedirect, null);
+		i.addEventListener("click", insertRedirect, false);
 	    }
-	for (let i of document.getElementsByTagName("a"))
+	for (let i of document.querySelectorAll("a[href]"))
 	    if (i.getAttribute("href").match(/^#[0-9]+$/))
 		if (RegExp.lastMatch == i.innerHTML)
-		    i.addEventListener("click", insertRedirect, null)
+		    i.addEventListener("click", insertRedirect, false)
+	for (let i of document.querySelectorAll('input[value="送信"]')) {
+	    // let b = document.createElement("button");
+	    let b = i;
+	    b.value = "送信!";
+	    console.log("b="+b+", tc="+b.textContent);
+	    b.addEventListener("click", ajaxPost, false);
+	    // i.insertAdjacentElement('afterend', b);
+	}
+	for (let f of document.querySelectorAll('input[type="file"]')) {
+	    let form = document.forms[0];
+	    f.addEventListener('change', (e) => {
+		warnFileSize(form);
+	    }, false)
+	}
+	// Hack article_m links
+	registPjaxViewers(document.querySelectorAll("a[href]"));
     }
     function initGrpAction() {
 	var rev = document.getElementById("reverse");
@@ -323,13 +671,6 @@
     function initGrphome() {
 	console.log("initGrphome");
 	// (1)Setup Frozen State Changing Button
-	let url = document.URL,
-	    mypath = url.substring(url.lastIndexOf("/"));
-	if (mypath.match(/(.*)\/(.*)/)) {
-	    mypath = RegExp.$2;
-	    mypath = mypath.substring(0, mypath.lastIndexOf("?"));
-	    //alert("mypath="+mypath);
-	} else return;
 	var ja = navigator.language.match(/ja/i);
 
 	function toggleFrozen(e, rowid) {
@@ -339,6 +680,7 @@
 	    fetch(tgt, {
 		method: "POST",
 		headers: {'Content-Type': 'text/html; charset=utf-8'},
+		credentials: "include"
 	    }).then(function(resp) {
 		return resp.text();
 	    }).then(function(tbody) {
@@ -420,6 +762,7 @@
 	}
     }
     function init() {
+	isOlderJS = !("insertAdjacentElement" in document.body);
 	initGrpAction();
 	initBlogs();
 	initFileInput();
--- a/scripts/s4-sns.case	Thu Dec 31 10:31:13 2020 +0900
+++ b/scripts/s4-sns.case	Sat Jan 02 15:15:03 2021 +0900
@@ -46,7 +46,7 @@
     echo "Refresh: 0; $newurl"; echo
     exit 0
     ;;
-  lshandout|lshandoutall|gethandout|gethandoutcsv|gethandoutcsv2|blogseen|getteamcsv|blog_setfrozen)
+  lshandout|lshandoutall|gethandout|gethandoutcsv|gethandoutcsv2|blogseen|getteamcsv|blog_setfrozen|blog_fetch)
     case "$stage" in
       lshandout*|blogseen*) contenttype; echo ;;
     esac

yatex.org