s4

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 diff
     1.1 --- a/examples/common/default/default.css	Thu Dec 31 10:31:13 2020 +0900
     1.2 +++ b/examples/common/default/default.css	Sat Jan 02 15:15:03 2021 +0900
     1.3 @@ -4,6 +4,9 @@
     1.4  body {background: #eff; margin: 2px; padding: 6px;}
     1.5  h2, h3, hr {clear: both;}
     1.6  *.warn {color: red;}
     1.7 +*.warnbg {background: red;}
     1.8 +*.hidden {visibility: hidden;}
     1.9 +*.border {border: 1px solid #113;}
    1.10  div.topmenu {
    1.11      margin: 0; padding: 0; width: 100%; height: 2em;
    1.12  }
    1.13 @@ -35,7 +38,9 @@
    1.14  table.td2r td:nth-child(2) {text-align: right;}
    1.15  table.td3r td:nth-child(3) {text-align: right;}
    1.16  table.td3rr td:nth-child(n+3) {text-align: right;}
    1.17 -table.td3evw th:nth-child(2n+4) {background: white;}
    1.18 +table.td3evw th:nth-child(2n+4), span.textdigest {
    1.19 +    background: white;
    1.20 +}
    1.21  table.thl th {text-align: left;}
    1.22  span#reverse {background: white; padding: 0 0 0 0.4ex; border: outset;}
    1.23  
    1.24 @@ -165,6 +170,24 @@
    1.25  table.mini th, table.mini td {text-align: justify;}
    1.26  table.mini td, table.mini th {padding: 1px 0.5ex; min-width: 1em;}
    1.27  table.mini {margin-left: 1em; border-left: 2px solid #686;}
    1.28 +div.pjaxview, div.pjaxview2 {
    1.29 +    position: fixed; top: 1ex; left: 0; width: 9vw; height: 9vh;
    1.30 +    background: #ffffee; border: 1px double navy; border-radius: 2em;
    1.31 +    z-index: 7;
    1.32 +}
    1.33 +div.pjaxview2 {
    1.34 +    width: 100vw; height: 95vw; transition: 0.5s;
    1.35 +}
    1.36 +div.pjaxview iframe {
    1.37 +    width: 98%; height: 90%; object-fit: scale-down;
    1.38 +}
    1.39 +div.pjaxview p {
    1.40 +    padding: 0.5ex; text-align: left; margin: 0 2em;
    1.41 +    white-space: nowrap; overflow: hidden;
    1.42 +}
    1.43 +div.pjaxview p button { font-size: large; padding: 0 1em;}
    1.44 +div.pjaxview p button { background: #eeeee0; }
    1.45 +div.pjaxview p button:focus { background: #fffff8; }
    1.46  
    1.47  /* "visibility: collapse" not working on chromium browser */
    1.48  div.noprofimg tr.profimg {visibility: collapse; display: none;}
    1.49 @@ -393,9 +416,13 @@
    1.50      border-radius: 1em; border: double 5px blue;
    1.51      min-width: 10em; right: 0; bottom: 0;
    1.52  }
    1.53 -.dissolving {
    1.54 -    opacity: 0; transition: 3.0s;
    1.55 +.dissolving {opacity: 0; transition: 3.0s;}
    1.56 +.emerging {opacity: 1; transition: 3s;}
    1.57 +span.loading, input#c:disabled  {
    1.58 +    visibility: visible; transform: rotateX(3600deg); transition: 30s;
    1.59 +    display: inline-block;
    1.60  }
    1.61 +span.loading {padding: 0 1em;}
    1.62  
    1.63  /*
    1.64   * PR Web
     2.1 --- a/s4-blog.sh	Thu Dec 31 10:31:13 2020 +0900
     2.2 +++ b/s4-blog.sh	Sat Jan 02 15:15:03 2021 +0900
     2.3 @@ -178,6 +178,7 @@
     2.4        fi
     2.5      fi
     2.6    fi
     2.7 +  err "blog_showentry  Entered: `gdate +%S.%03N` blogrowid=$rowid"
     2.8    blog_notify=`getvalbyid blog notify "$rowid"`
     2.9    blog_team=`blog_getteam "$rowid"`
    2.10    blog_mode=`getvalbyid blog mode "$rowid"`
    2.11 @@ -201,6 +202,12 @@
    2.12  		THEN ''
    2.13  		ELSE 'Unreadable'
    2.14  		END"
    2.15 +	  elif [ x"$blog_mode" = x"report-closed" ]; then
    2.16 +	    F_UNREADABLE="CASE
    2.17 +		WHEN author = '$user'
    2.18 +		THEN ''
    2.19 +		ELSE 'Unreadable'
    2.20 +		END"
    2.21  	  else
    2.22  	    F_UNREADABLE="'Unreadable'"
    2.23  	  fi
    2.24 @@ -226,6 +233,21 @@
    2.25    #err id=$id
    2.26    #err "select val from $ts where key='title' and id='$id';"
    2.27  
    2.28 +  ## Parse control sequence
    2.29 +  nlimit=$listartlimit
    2.30 +  case "$control" in
    2.31 +    n:[Aa][Ll][Ll])
    2.32 +      unset nlimit ;;
    2.33 +    n:*)
    2.34 +      nlimit=${control##*:}
    2.35 +      nlimit=${nlimit%%[!A-Z0-9a-z_]*}
    2.36 +      ;;
    2.37 +    f:[Aa][Ll][Ll])	;;
    2.38 +    f:2???-??-??*)	# f:2020-12-27T08:02:43
    2.39 +      fetch=${control#f:}
    2.40 +      fetch_ajax=`echo "'$fetch'"|tr T ' '`
    2.41 +  esac
    2.42 +  err control=$control fetch_ajax=$fetch_ajax
    2.43    #(1)Display root article
    2.44    cat<<EOF
    2.45  <form class="replyblog" action="$myname?replyblog+${rowid}#bottom" method="POST" enctype="multipart/form-data">
    2.46 @@ -271,18 +293,21 @@
    2.47  	EOF
    2.48    if test -s $midfile && IFS='|' read edit ctime hexhead blogtype < $midfile
    2.49    then
    2.50 -    cat<<-EOF
    2.51 +    if [ -z "$fetch_ajax" ]; then	# UUUUU
    2.52 +
    2.53 +      cat<<-EOF
    2.54  	<tr><td>${edit:+$href }$ctime $blogtype $href2${edit:+$href3} $href4 $href5</td></tr>
    2.55  	<tr class="preface${frozen_class:+ }$frozen_class">
    2.56  	 <td>`echo "$hexhead"|unhexize|htmlescape|hreflink|minitbl`</td></tr>
    2.57  	</table>
    2.58  	EOF
    2.59 -    case "$blogtype" in
    2.60 -      "クイズ"|"XXXX集計")
    2.61 -	echo "${blogtype}モードは本人と管理者の書き込みのみが表示されます。"
    2.62 -	;;
    2.63 -    esac | html p 'class="warn"'
    2.64 +      case "$blogtype" in
    2.65 +	"クイズ"|"XXXX集計")
    2.66 +	  echo "${blogtype}モードは本人と管理者の書き込みのみが表示されます。"
    2.67 +	  ;;
    2.68 +      esac | html p 'class="warn"'
    2.69  
    2.70 +    fi			# UUUUU
    2.71      if [ x"$blogtype" = x"クイズ" -o x"$blogtype" = x"XXXX集計" ]; then
    2.72        if $isgroup; then
    2.73  	# Failsafe to query timeout
    2.74 @@ -307,16 +332,7 @@
    2.75      echo "時間をおいて繋いでください(Please visit later)." | html p
    2.76      return
    2.77    fi
    2.78 -  ## Parse control sequence
    2.79 -  nlimit=$listartlimit
    2.80 -  case "$control" in
    2.81 -    n:[Aa][Ll][Ll])
    2.82 -	unset nlimit ;;
    2.83 -    n:*)
    2.84 -	nlimit=${control##*:}
    2.85 -	nlimit=${nlimit%%[!A-Z0-9a-z_]*}
    2.86 -	;;
    2.87 -  esac
    2.88 +
    2.89    lkhome="<a href=\"$myname?home" lke='">'
    2.90    lkedit="<a href=\"$myname?editart"
    2.91    hlink="$myname?home" elink="$myname?editart"
    2.92 @@ -392,7 +408,8 @@
    2.93        $cond_qz) a
    2.94    LEFT JOIN
    2.95       a_s s
    2.96 -  ON a.id=s.id;
    2.97 +  ON a.id=s.id
    2.98 +  ${fetch_ajax:+WHERE s.TIME > $fetch_ajax};
    2.99  EOF
   2.100    if [ $? -ne 0 -a ! -s $midfile ]; then
   2.101      echo "時間をおいてください(Visit later please)." | html p
   2.102 @@ -426,6 +443,7 @@
   2.103    else
   2.104      CAT=cat
   2.105    fi
   2.106 +  err "blog_showentry  Started: `gdate +%S.%03N` ${fetch_ajax:+ajax}"
   2.107    # Start blog_replies table
   2.108    $CAT $midfile |
   2.109    while IFS='|' read id edit notify uid author uname icon aid \
   2.110 @@ -487,6 +505,7 @@
   2.111  	usecache='' tsfile=$td/$id.stamp
   2.112  	for i in $imgids; do
   2.113  	  mrid=${i%%:*}; i=${i#*:}; sz=`size_h ${i%%:*}`
   2.114 +	  _href="href=\"$catlink+$mrid\""
   2.115  	  fn=`echo "${i#*:}"|unhexize`
   2.116  	  fnb=$fn"(${sz})"
   2.117  	  case "$fn" in
   2.118 @@ -508,7 +527,7 @@
   2.119  			   };}; then
   2.120  		usecache=1		# Set usecache flag on
   2.121  		cat<<-EOF
   2.122 -		<a href="$catlink+$mrid"><img src="$outfile">
   2.123 +		<__UNCLICKABLE__><a $_href><img src="$outfile">
   2.124  		$fnb</a>
   2.125  		EOF
   2.126  		# !!NOTE!! Create row stamp ONLY WHEN imgcache is active
   2.127 @@ -522,18 +541,18 @@
   2.128  		  cat "$outfile" \
   2.129  		    | hexize \
   2.130  		    | sed -e 's/\(..\)/%\1/g' \
   2.131 -	    		  -e "s|^|<a href=\"$catlink+$mrid\"><img src=\"data:image/$fmt,|" \
   2.132 +	    		  -e "s|^|<__UNCLICKABLE__><a $_href><img src=\"data:image/$fmt,|" \
   2.133  			  -e "s|\$|\">$fnb</a>|"
   2.134  		  unset stampfile # img data stream is not suitable to cache
   2.135  		  echo $tm > $tsfile
   2.136  		else	# Failed to convert
   2.137  		  rm -f $outfile
   2.138 -		  echo "<a href=\"$catlink+$mrid\">$fnb</a>"
   2.139 +		  echo "<__UNCLICKABLE__><a $_href>$fnb</a>"
   2.140  		fi
   2.141  	      fi
   2.142  	      ;;
   2.143  	    *)
   2.144 -	      echo "<__UNREADABLE__><a href=\"$catlink+$mrid\"><img src=\"$deficon\">$fnb</a>"
   2.145 +	      echo "<__UNCLICKABLE__><a $_href><img src=\"$deficon\">$fnb</a>"
   2.146  	      ;;
   2.147  	  esac
   2.148  	done
   2.149 @@ -548,10 +567,18 @@
   2.150        fi
   2.151        test -n "$stampfile" && date "+%F %T" > $stampfile
   2.152      fi
   2.153 +    if [ -n "$fa" ]; then
   2.154 +      replhref="s/a href=[^>]*>/a>/"
   2.155 +    else
   2.156 +      replhref=""
   2.157 +    fi
   2.158      # Printing a cached row
   2.159      sed -e "/^<td class=/s/__NEWCLS__/$new${new:+ }/" \
   2.160  	-e "/^<td class=/s,__EDIT__,$editlink," \
   2.161  	-e "/^<__NOTIFY__>/s,,${notify:+$nt}," \
   2.162 +	${replhref:+-e "/^<__UNCLICKABLE__>/$replhref"} \
   2.163 +	${replhref:+-e "/^<__UNREADABLE__>/$replhref"} \
   2.164 +	-e "/<__UNCLICKABLE__>/s///" \
   2.165  	-e "/<__UNREADABLE__>/s,,${fa:+$cannotread}," \
   2.166  	$cachefile
   2.167    done
   2.168 @@ -589,10 +616,12 @@
   2.169  `cgi_file image "" "$file_accept title=\"$filehelp\" multiple"`
   2.170  </td></tr>
   2.171  </table>
   2.172 -<input type="submit" value="送信" class="$blog_notify" title="$ntmode">
   2.173 +<input type="hidden" name="fetchtime" value="`date +%FT%T`">
   2.174 +<input type="hidden" name="filesize_max" value="$filesize_max">
   2.175 +<input type="submit" id="c" value="送信" class="$blog_notify" title="$ntmode">
   2.176  <input type="reset" value="リセット"></div></div>
   2.177  EOF
   2.178 -	)
   2.179 +	  )
   2.180    cat<<-EOF
   2.181  	</table> <!-- end of s4-blog:blog_showentry() main table -->
   2.182  	<p class="update_link"><a
   2.183 @@ -615,6 +644,7 @@
   2.184    [ -s $iconcleaner ] && query ".read '$iconcleaner'"
   2.185    # Record access log
   2.186    acclog blog $rowid
   2.187 +  err "blog_showentry Finished: `gdate +%S.%03N` ${fetch_ajax:+ajax}"
   2.188  }
   2.189  
   2.190  lshandout() {
   2.191 @@ -1522,6 +1552,13 @@
   2.192    err "blog_reply Finished: `gdate +%S.%03N` user=$user owner=[$owner] title=[$title]"
   2.193  }
   2.194  
   2.195 +blog_fetch() {
   2.196 +  contenttype "text/plain; charset=utf-8"; echo
   2.197 +  err blog_fetch: blog "$@"
   2.198 +  blog_reply "$@"
   2.199 +  # blog_showentry blog "$@"
   2.200 +}
   2.201 +
   2.202  blog_reply_article() {		# Direct link to article in some blog
   2.203    arid=${1:-0}			# Already sanitized to digits
   2.204    brid=`query "SELECT rowid FROM blog WHERE \
     3.1 --- a/s4-main.js	Thu Dec 31 10:31:13 2020 +0900
     3.2 +++ b/s4-main.js	Sat Jan 02 15:15:03 2021 +0900
     3.3 @@ -1,5 +1,16 @@
     3.4  // 愛
     3.5  (function (){
     3.6 +    var isOlderJS;	// Set in init();
     3.7 +    var hasTouchPad =
     3.8 +	(navigator.maxTouchPoints && navigator.maxTouchPoints >0);
     3.9 +    var myurl = document.URL,
    3.10 +	mypath = myurl.substring(myurl.lastIndexOf("/"));
    3.11 +    var art_m_list = [];
    3.12 +    if (mypath.match(/(.*)\/(.*)/)) {
    3.13 +	mypath = RegExp.$2;
    3.14 +	mypath = mypath.substring(0, mypath.lastIndexOf("?"));
    3.15 +	//alert("mypath="+mypath);
    3.16 +    }
    3.17      function collectElementsByAttr(elm, attr, val) {
    3.18  	var e = document.getElementsByTagName(elm);
    3.19  	if (!e) return null;
    3.20 @@ -67,6 +78,325 @@
    3.21  	    textarea.value = lines.join("\n");
    3.22  	}
    3.23      }
    3.24 +    function registPjaxViewers(aHrefList) {
    3.25 +	// if (isOlderJS) return;
    3.26 +	let apos=art_m_list.length;
    3.27 +	for (let a of aHrefList) {
    3.28 +	    let href = a.getAttribute("href");
    3.29 +	    let localvar = apos;
    3.30 +	    let td = a.parentNode,
    3.31 +		tr = td.parentNode,
    3.32 +		id = td.id,
    3.33 +		text = td.textContent,
    3.34 +		author = tr.getElementsByTagName("a");
    3.35 +	    if (author) author = author[0].getAttribute("title");
    3.36 +	    if (href.match(/\?showattc\+article_m\+([0-9+])/)) {
    3.37 +		if (td.innerHTML.match(/x読み取り不可/)) {
    3.38 +		    a.removeAttribute("href");
    3.39 +		    continue;
    3.40 +		}
    3.41 +		let url = RegExp.lastMatch;
    3.42 +		// console.log("pjaxView(e, "+href+", "+apos+")");
    3.43 +		a.addEventListener("click", function(e) {
    3.44 +		    // Shoud use closure local variable: localvar
    3.45 +		    pjaxView(e, href, localvar);
    3.46 +		}, false);
    3.47 +		apos++;
    3.48 +		art_m_list.push({
    3.49 +		    url: href, id: id, author: author, text: text
    3.50 +		});
    3.51 +	    }
    3.52 +	}
    3.53 +    }
    3.54 +    var ajaxSubmit;
    3.55 +    function replAddNews(newtable) {
    3.56 +	let newids = [], idlist=[];
    3.57 +	let getArticleID = function (td) {
    3.58 +	    return parseInt(td.parentNode.getElementsByTagName("td")[1].id);
    3.59 +	}
    3.60 +	for (let i of newtable.querySelectorAll("td.repl"))
    3.61 +	    newids.push(i);
    3.62 +	newids = newids.sort((a,b)=> {
    3.63 +	    return (getArticleID(a) - getArticleID(b));
    3.64 +	});
    3.65 +	for (i of newids)
    3.66 +	    idlist.push(getArticleID(i));
    3.67 +	console.log("IDList="+idlist.join());
    3.68 +	let cnt=0;
    3.69 +	let current = collectElementsByAttr("td", "class", "repl"),
    3.70 +	    ncur=0, n, icur=0, o, oid, nid, otr;
    3.71 +	current = document.querySelectorAll('td[class="repl"]');
    3.72 +	let last=current[current.length-1],
    3.73 +	    tbody = last.parentNode.parentNode;
    3.74 +	// Now reconstruct articles with merge-sort like method
    3.75 +	outer: for (; ncur<newids.length; ncur++) {
    3.76 +	    n = newids[ncur];
    3.77 +	    if (!n.id) continue;
    3.78 +	    nid = parseInt(n.id);
    3.79 +	    if (nid<=0) continue;
    3.80 +	    let ntr = n.parentNode;
    3.81 +	    for (; icur<current.length; icur++) {
    3.82 +		o = current[icur];
    3.83 +		otr = o.parentNode;
    3.84 +		oid = getArticleID(o);
    3.85 +		if (!oid || oid=="") continue;
    3.86 +		if (oid >= nid) {
    3.87 +		    ntr.getElementsByTagName("td")[0].classList.add("new");
    3.88 +		    tbody.insertBefore(ntr, otr);
    3.89 +		    if (oid==nid) otr.remove();
    3.90 +		    cnt++;
    3.91 +		    continue outer;
    3.92 +		}
    3.93 +	    }
    3.94 +	    // Append absolutely new articles.
    3.95 +	    ntr = n.parentNode;
    3.96 +	    ntr.getElementsByTagName("td")[0].classList.add("new");
    3.97 +	    tbody.appendChild(ntr);
    3.98 +	    registPjaxViewers(ntr.querySelectorAll("a[href]"));
    3.99 +	    ntr.classList.add("dissolving");
   3.100 +	    ntr.scrollIntoView({behavior: "smooth"});
   3.101 +	    setTimeout(() => {
   3.102 +		ntr.classList.remove("dissolving");
   3.103 +		ntr.classList.add("emerging");
   3.104 +	    }, 100);
   3.105 +	    cnt++;
   3.106 +	}
   3.107 +	ajaxSubmit.value = ajaxSubmit.back;
   3.108 +	ajaxSubmit.disabled = false;
   3.109 +	console.log("Update "+cnt+"rows");
   3.110 +	return cnt;
   3.111 +    }
   3.112 +
   3.113 +    function warnFileSize(form) {
   3.114 +	let szmax = form.querySelector('input[name="filesize_max"]').value;
   3.115 +	if (!szmax || szmax=="") return;
   3.116 +	szmax = parseInt(szmax);
   3.117 +	if (szmax <= 0) return;
   3.118 +	// szmax = 10000
   3.119 +	let ng = "", rcval=false, fileexists=false;
   3.120 +	for (let f of form.querySelectorAll('input[type="file"]')) {
   3.121 +	    let thiserr = false
   3.122 +	    for (let i of f.files) {
   3.123 +		fileexists = true;
   3.124 +		let fn = i.name, sz = i.size;
   3.125 +		console.log("max="+szmax+", fn="+fn+", sz="+sz);
   3.126 +		if (sz > szmax) {
   3.127 +		    thiserr = true;
   3.128 +		    ng += ((ng>"" ? ", " : "")+fn)
   3.129 +		}
   3.130 +	    }
   3.131 +	    thiserr ? f.classList.add("warnbg") : f.classList.remove("warnbg");
   3.132 +	}
   3.133 +	if (ng>"") {
   3.134 +	    rcval = "File-size Limit Error: "+ng+"\n"+
   3.135 +		"Should be less than "+szmax+"bytes.\n"+
   3.136 +		szmax+"バイト未満にしてください"
   3.137 +	    alert(rcval);
   3.138 +	}
   3.139 +	if (form.text.value == "") {
   3.140 +	    let w;
   3.141 +	    if (fileexists)
   3.142 +		w = "Fill the text area\n" +
   3.143 +		"添付したファイルに関する説明を入れてください。";
   3.144 +	    else
   3.145 +		w = "Enter your comment!\n何か書き込んでね!";
   3.146 +	    alert(w);
   3.147 +	    rcval = (rcval || w);
   3.148 +	    form.text.classList.add("warnbg");
   3.149 +	    setTimeout(() => {form.text.classList.remove("warnbg");}, 2000)
   3.150 +	}
   3.151 +	return rcval;
   3.152 +    }
   3.153 +    function ajaxPost(e) {
   3.154 +	e.preventDefault();
   3.155 +	let rowid;
   3.156 +	if (!myurl.match(/replyblog\+([0-9]+)/)) return;
   3.157 +	rowid = RegExp.$1
   3.158 +	let myform = document.querySelector("form.replyblog");
   3.159 +	if (warnFileSize(myform)) return;
   3.160 +	ajaxSubmit = e.target;
   3.161 +	ajaxSubmit.back = ajaxSubmit.value;
   3.162 +	ajaxSubmit.value = "送信中";
   3.163 +	ajaxSubmit.blur();
   3.164 +	ajaxSubmit.disabled = true;
   3.165 +	let data = new FormData(myform),
   3.166 +	    fetchtime = data.get("fetchtime");
   3.167 +	if (!fetchtime || fetchtime=="") return;
   3.168 +	///*XX*/fetchtime = "2020-06-14T00:00:00";data.set("fetchtime", fetchtime)
   3.169 +	let act = mypath+"?blog_fetch+"+rowid+"+f:"+fetchtime;
   3.170 +	
   3.171 +	function respUpdate(tbody) {
   3.172 +	    let div = document.createElement("div"), form, newform;
   3.173 +	    try {
   3.174 +		div.innerHTML = tbody;
   3.175 +		form = div.querySelector("form");
   3.176 +	    } catch (er) {
   3.177 +		alert("Cannot parse fetch data");
   3.178 +		return;
   3.179 +	    }
   3.180 +	    let update = replAddNews(form);
   3.181 +	    let dispelem = myform.querySelector("textarea").parentNode;
   3.182 +	    newform = new FormData(form);
   3.183 +	    myform.reset();
   3.184 +	    myform.text.value = '';
   3.185 +	    myform.fetchtime.value = newform.get("fetchtime");
   3.186 +	    myform.id.value = newform.get("id");
   3.187 +	    if (update && update > 0) {
   3.188 +		let s = update + " new article" +
   3.189 +		    (update>1 ? "s" : "") + " posted";
   3.190 +		dispInfoMomentary(s, dispelem);
   3.191 +	    }
   3.192 +	}
   3.193 +	fetch(act, {
   3.194 +	    method: "POST", body: data,
   3.195 +	    credentials: "include"	// For older firefox
   3.196 +	}).then((resp) => {
   3.197 +	    return resp.text();
   3.198 +	}).then((tbody) => {
   3.199 +	    respUpdate(tbody);
   3.200 +	})
   3.201 +    }
   3.202 +    function pjaxView(ev, url, mynum) {
   3.203 +	ev.preventDefault();
   3.204 +	let box = document.createElement("div")
   3.205 +	box.setAttribute("class", "pjaxview");
   3.206 +	let p1 = document.createElement("p"),
   3.207 +	    bt = document.createElement("button"),
   3.208 +	    sl = document.createElement("button"),
   3.209 +	    sr = document.createElement("button"),
   3.210 +	    loading = document.createElement("span"),
   3.211 +	    info   = document.createElement("p");
   3.212 +	    info1  = document.createElement("span");
   3.213 +	    info2  = document.createElement("span");
   3.214 +	    iframe = document.createElement("iframe");
   3.215 +	var curpos = mynum;
   3.216 +	var historyBase = history.length;
   3.217 +	
   3.218 +	function _setPjaxCurposInfo() {
   3.219 +	    let len = art_m_list.length;
   3.220 +	    let cur = art_m_list[curpos]
   3.221 +	    info1.textContent = (1+curpos)+" of "+len+" article #"+cur.id+
   3.222 +		(cur.author ? " by "+cur.author : "") + ":";
   3.223 +	    info2.textContent = cur.text.trim();
   3.224 +	    info2.setAttribute("class", "border textdigest");
   3.225 +	}
   3.226 +	function _resetPjax() {
   3.227 +	    // All we can do surely is to back 1 page,
   3.228 +	    // because we cannot move to desirable entry of history list.
   3.229 +	    history.back();
   3.230 +	}
   3.231 +	function setSwipeAct(iframe) {
   3.232 +	    // We cannot use DOMContentLoaded nor iframe.contentWindow here.
   3.233 +	    // PDF.js does not construct contentWindow...?
   3.234 +	    iframe.addEventListener("load", () => {
   3.235 +		loading.classList.remove("loading");
   3.236 +		if (!hasTouchPad) return;
   3.237 +		let ifm = iframe.contentDocument;
   3.238 +		let startX, moveX, thresh = 100;
   3.239 +		ifm.addEventListener("touchstart", (e) => {
   3.240 +		    e.preventDefault();
   3.241 +		    startX = e.touches[0].pageX;
   3.242 +		}, false);
   3.243 +		ifm.addEventListener("touchmove", (e) => {
   3.244 +		    e.preventDefault();
   3.245 +		    moveX = e.touches[0].pageX;
   3.246 +		}, false);
   3.247 +		ifm.addEventListener("touchend", (e) => {
   3.248 +		    if (startX < moveX && startX + thresh < moveX) {
   3.249 +			switchTo(e, -1);
   3.250 +		    } else if (startX > moveX && startX - thresh > moveX) {
   3.251 +			switchTo(e, +1);
   3.252 +		    }
   3.253 +		}, false);
   3.254 +	    }, false);
   3.255 +	    
   3.256 +	}
   3.257 +	function switchTo(e, direction) {
   3.258 +	    e.preventDefault();
   3.259 +	    let len = art_m_list.length, cur, newpos, url;
   3.260 +	    newpos = (curpos+len+direction)%len;
   3.261 +	    if (curpos == newpos) return; // No need to switch to same one
   3.262 +	    curpos = newpos;
   3.263 +	    cur = art_m_list[curpos];
   3.264 +	    url = cur.url;
   3.265 +	    // We should remove iframe once to preserve history Object
   3.266 +	    // https://inthetechpit.com/2019/04/20/update-iframe-without-affecting-browser-history/
   3.267 +	    let parent = iframe.parentNode;
   3.268 +	    // alert("D = "+direction);
   3.269 +	    iframe.remove();
   3.270 +	    parent.appendChild(iframe);
   3.271 +	    try {
   3.272 +		loading.classList.add("loading");
   3.273 +		iframe.src = url;
   3.274 +		// iframe.contentDocument.location.replace(url);
   3.275 +		// location.replace cannot be used because PDF viewer.js
   3.276 +		// does not have iframe.contentDocument
   3.277 +	    } catch (err) {
   3.278 +		alert("Cannot load "+src+" : "+err.name);
   3.279 +	    }
   3.280 +	    _setPjaxCurposInfo();
   3.281 +	    setSwipeAct(iframe);
   3.282 +	}
   3.283 +	function switchToByKey(e) {
   3.284 +	    // alert("KEY="+e.key);
   3.285 +	    switch (e.key) {
   3.286 +	    case "ArrowLeft":
   3.287 +		switchTo(e, -1); break;
   3.288 +	    case "ArrowRight":
   3.289 +		switchTo(e, +1); break;
   3.290 +	    case "Escape":
   3.291 +		history.back();
   3.292 +	    }
   3.293 +	}
   3.294 +	// <div><p>
   3.295 +	// <button> << </button><button>Dismiss</button><button> >> </button>
   3.296 +	// </p><p><span> info1 </span> <span> info2 </span></p>
   3.297 +	// <iframe src="..."></iframe>
   3.298 +	// </div>
   3.299 +	// ==> [ << ][Dissmiss][ >> ]
   3.300 +	// ==> ## of ## article #xxx by AUTHOR
   3.301 +	sl.textContent = " << ";
   3.302 +	sr.textContent = " >> ";
   3.303 +	sl.addEventListener("click", (e) => {switchTo(e, -1);});
   3.304 +	sr.addEventListener("click", (e) => {switchTo(e, +1);});
   3.305 +	sl.setAttribute("title", "to="+(mynum-1));
   3.306 +	sr.setAttribute("title", "to="+(mynum+1));
   3.307 +	document.body.appendChild(box);
   3.308 +	bt.textContent = "Click to dismiss / もどる"+mynum;
   3.309 +
   3.310 +	box.appendChild(p1);
   3.311 +	p1.appendChild(sl); p1.appendChild(bt); p1.appendChild(sr);
   3.312 +	p1.appendChild(loading);
   3.313 +	info.appendChild(info1); info.appendChild(info2);
   3.314 +	loading.textContent=" Loading...";
   3.315 +	loading.classList.add("hidden");
   3.316 +	loading.classList.add("loading");
   3.317 +	box.appendChild(info);
   3.318 +	iframe.src = url;
   3.319 +
   3.320 +	document.addEventListener("keydown", switchToByKey);
   3.321 +	//box.addEventListener("click", (e) => {_resetPjax();});
   3.322 +	bt.addEventListener("click", (e) => {_resetPjax();});
   3.323 +	// dp.addEventListener("click", (e) => {_resetPjax();});
   3.324 +	info.addEventListener("click", (e) => {_resetPjax();});
   3.325 +	box.appendChild(iframe);
   3.326 +
   3.327 +	setSwipeAct(iframe);
   3.328 +
   3.329 +	_setPjaxCurposInfo();
   3.330 +	bt.focus();
   3.331 +	setTimeout(() => {box.classList.add("pjaxview2");}, 10);
   3.332 +	// Finally update history stack
   3.333 +	if (history.pushState) {
   3.334 +	    let h = location.href.replace(/#.*/, '')+"#pjaxview";
   3.335 +	    history.pushState({url: h}, null, h);
   3.336 +	    window.addEventListener("popstate", (e) => {
   3.337 +		if (box) {
   3.338 +		    box.remove(); box = null;
   3.339 +		}
   3.340 +	    }, false);
   3.341 +	}
   3.342 +    }
   3.343      function reverseChecks() {
   3.344  	var names = collectElementsByAttr("input", "name", "usel");
   3.345  	for (let u of names) {
   3.346 @@ -75,7 +405,6 @@
   3.347      }
   3.348      function renumberOL(str, start) {
   3.349  	var stra = str.split("\n");
   3.350 -		
   3.351  	for (var i=1; i<stra.length; i++) {
   3.352  	    if (stra[i].match(/^[1-9][0-9]*\. /)) {
   3.353  		let orig=stra[i];
   3.354 @@ -192,9 +521,9 @@
   3.355  	}
   3.356      }
   3.357      function helpMarkdown(e) {
   3.358 -	switch (e.keyCode) {
   3.359 -	case  8: helpMarkdownBS(e); break;
   3.360 -	case 13: helpMarkdownEnter(e); break;
   3.361 +	switch (e.key) {
   3.362 +	case "Backspace": helpMarkdownBS(e); break;
   3.363 +	case "Enter":  helpMarkdownEnter(e); break;
   3.364  	}
   3.365      }
   3.366      /* Init event listeners */
   3.367 @@ -212,7 +541,10 @@
   3.368  		var inpf = ih.substring(ih.indexOf("<input")),
   3.369  		    newi = "<br>"+inpf.substring(0, inpf.indexOf(">")+1);
   3.370  		i.insertAdjacentHTML("afterend", newi)
   3.371 -		// alert(newi);
   3.372 +		i.nextSibling.nextSibling.addEventListener('change', () => {
   3.373 +		    // next==br next.next==input[type=file]
   3.374 +		    warnFileSize(document.forms[0]);
   3.375 +		});
   3.376  	    }
   3.377  	}
   3.378      }
   3.379 @@ -248,12 +580,28 @@
   3.380  	var check = collectElementsByAttr("input", "name", "notifyto");
   3.381  	if (check)
   3.382  	    for (let i of check) {
   3.383 -		i.addEventListener("click", insertRedirect, null);
   3.384 +		i.addEventListener("click", insertRedirect, false);
   3.385  	    }
   3.386 -	for (let i of document.getElementsByTagName("a"))
   3.387 +	for (let i of document.querySelectorAll("a[href]"))
   3.388  	    if (i.getAttribute("href").match(/^#[0-9]+$/))
   3.389  		if (RegExp.lastMatch == i.innerHTML)
   3.390 -		    i.addEventListener("click", insertRedirect, null)
   3.391 +		    i.addEventListener("click", insertRedirect, false)
   3.392 +	for (let i of document.querySelectorAll('input[value="送信"]')) {
   3.393 +	    // let b = document.createElement("button");
   3.394 +	    let b = i;
   3.395 +	    b.value = "送信!";
   3.396 +	    console.log("b="+b+", tc="+b.textContent);
   3.397 +	    b.addEventListener("click", ajaxPost, false);
   3.398 +	    // i.insertAdjacentElement('afterend', b);
   3.399 +	}
   3.400 +	for (let f of document.querySelectorAll('input[type="file"]')) {
   3.401 +	    let form = document.forms[0];
   3.402 +	    f.addEventListener('change', (e) => {
   3.403 +		warnFileSize(form);
   3.404 +	    }, false)
   3.405 +	}
   3.406 +	// Hack article_m links
   3.407 +	registPjaxViewers(document.querySelectorAll("a[href]"));
   3.408      }
   3.409      function initGrpAction() {
   3.410  	var rev = document.getElementById("reverse");
   3.411 @@ -323,13 +671,6 @@
   3.412      function initGrphome() {
   3.413  	console.log("initGrphome");
   3.414  	// (1)Setup Frozen State Changing Button
   3.415 -	let url = document.URL,
   3.416 -	    mypath = url.substring(url.lastIndexOf("/"));
   3.417 -	if (mypath.match(/(.*)\/(.*)/)) {
   3.418 -	    mypath = RegExp.$2;
   3.419 -	    mypath = mypath.substring(0, mypath.lastIndexOf("?"));
   3.420 -	    //alert("mypath="+mypath);
   3.421 -	} else return;
   3.422  	var ja = navigator.language.match(/ja/i);
   3.423  
   3.424  	function toggleFrozen(e, rowid) {
   3.425 @@ -339,6 +680,7 @@
   3.426  	    fetch(tgt, {
   3.427  		method: "POST",
   3.428  		headers: {'Content-Type': 'text/html; charset=utf-8'},
   3.429 +		credentials: "include"
   3.430  	    }).then(function(resp) {
   3.431  		return resp.text();
   3.432  	    }).then(function(tbody) {
   3.433 @@ -420,6 +762,7 @@
   3.434  	}
   3.435      }
   3.436      function init() {
   3.437 +	isOlderJS = !("insertAdjacentElement" in document.body);
   3.438  	initGrpAction();
   3.439  	initBlogs();
   3.440  	initFileInput();
     4.1 --- a/scripts/s4-sns.case	Thu Dec 31 10:31:13 2020 +0900
     4.2 +++ b/scripts/s4-sns.case	Sat Jan 02 15:15:03 2021 +0900
     4.3 @@ -46,7 +46,7 @@
     4.4      echo "Refresh: 0; $newurl"; echo
     4.5      exit 0
     4.6      ;;
     4.7 -  lshandout|lshandoutall|gethandout|gethandoutcsv|gethandoutcsv2|blogseen|getteamcsv|blog_setfrozen)
     4.8 +  lshandout|lshandoutall|gethandout|gethandoutcsv|gethandoutcsv2|blogseen|getteamcsv|blog_setfrozen|blog_fetch)
     4.9      case "$stage" in
    4.10        lshandout*|blogseen*) contenttype; echo ;;
    4.11      esac