s4

view s4-main.js @ 1013:1ffaa8b2b1bf

Modify blog comment form layout
author HIROSE Yuuji <yuuji@gentei.org>
date Tue, 11 Jul 2023 10:25:21 +0900
parents 5bd1b5125049
children e8f73df7ed5d
line source
1 // 愛
2 (function (){
3 var isOlderJS; // Set in init();
4 var hoverTextLines = 10;
5 var hasTouchPad =
6 (navigator.maxTouchPoints && navigator.maxTouchPoints >0);
7 var myurl = document.URL,
8 mypath = myurl.substring(myurl.lastIndexOf("/"));
9 var art_m_list = [];
10 var mathjax = false;
11 let input_pdfsw = 'input[name="comppdf"]';
12 if (mypath.match(/(.*)\/(.*)/)) {
13 mypath = RegExp.$2;
14 mypath = mypath.substring(0, mypath.lastIndexOf("?"));
15 //alert("mypath="+mypath);
16 }
17 function escapeChars(old) {
18 return old.replaceAll('&', '&amp;')
19 .replaceAll('"', '&quot;')
20 .replaceAll("<", '&lt;')
21 .replaceAll(">", '&gt;');
22 }
23 function collectElementsByAttr(elm, attr, val) {
24 var e = document.getElementsByTagName(elm);
25 if (!e) return null;
26 var list = [];
27 for (var i of e) {
28 if (i.getAttribute(attr) == val)
29 list.push(i)
30 }
31 return list;
32 }
33 function nthChildOf(parent, n, elem) { // Return Nth child of type ELEM
34 // N begins with 1
35 var i=0;
36 var le = elem.toLowerCase();
37 for (var c of parent.childNodes) {
38 if (!c.tagName) continue;
39 if (c.tagName.toLowerCase() == le) {
40 if (++i >= n) return c;
41 }
42 }
43 return null;
44 }
45 function insertRedirect(e) {
46 var articleId, textarea = document.getElementById("text");
47 var p = e.target, checked = p.checked;
48 while (p = p.parentNode)
49 if (p.nodeName.match(/^td$/i)) break;
50 if (!p) return;
51 while (p = p.nextSibling)
52 if (p.nodeName.match(/^td$/i)) break;
53 if (!p) return;
54 articleId = p.getAttribute("id");
55 if (textarea && articleId) {
56 var tv = textarea.value, lines;
57 if (tv)
58 lines = tv.split("\n");
59 else
60 lines = [""];
61 var re = new RegExp("[, ]*#"+articleId+"(?![0-9])");
62 checked = (p.nodeName.match(/^input$/)
63 ? p.checked // checkbox obeys its status
64 : !lines[0].match(re)) // a-elment toggles redirection
65 if (checked) {
66 if (!lines[0].match(re)) {
67 var re2 = new RegExp(/>#[#0-9, ]+[0-9]/);
68 if (lines[0].match(re2))
69 lines[0] = lines[0].replace(
70 re2, '$&, '+'#'+articleId);
71 else {
72 if (lines[0] > "") lines[0] = " "+lines[0];
73 lines[0] = ">#"+articleId+lines[0];
74 }
75 }
76 } else { // Remove #xxxxx
77 if (lines[0].match(/^>#[0-9 ,]+#/)) // 2 or more #id's
78 lines[0] = lines[0].replace(
79 new RegExp("^>#"+articleId+"[ ,]*"), ">").replace(
80 new RegExp("[ ,]*#"+articleId), "");
81 else {
82 lines[0] = lines[0].replace(
83 new RegExp(">#"+articleId+"[ ,]*"), "");
84 }
85 }
86 lines[0] = lines[0].replace(/^> *$/, '');
87 textarea.value = lines.join("\n");
88 }
89 }
90 function registPjaxViewers(aHrefList) {
91 let apos=art_m_list.length;
92 for (let a of aHrefList) {
93 let href = a.getAttribute("href");
94 let localvar = apos;
95 let td = a.parentNode,
96 tr = td.parentNode,
97 id = td.id,
98 text = td.textContent,
99 author = tr.getElementsByTagName("a");
100 if (author) author = author[0].getAttribute("title");
101 if (href.match(/\?showattc\+article_m\+([0-9]+)$/)) {
102 if (td.innerHTML.match(/読み取り不可/)) {
103 a.removeAttribute("href");
104 continue;
105 }
106 let url = RegExp.lastMatch;
107 // console.log("pjaxView(e, "+href+", "+apos+")");
108 a.addEventListener("click", function(e) {
109 // Shoud use closure local variable: localvar
110 pjaxView(e, href, localvar);
111 }, false);
112 apos++;
113 art_m_list.push({
114 url: href, id: id, author: author, text: text
115 });
116 }
117 }
118 }
119 function registInsertDirect(aHrefList) {
120 for (i of aHrefList)
121 if (i.getAttribute("href").match(/^#[0-9]+$/))
122 if (RegExp.lastMatch == i.innerHTML)
123 i.addEventListener("click", insertRedirect, false)
124 }
125 function mathjaxUpdate(arg) {
126 try {
127 if (MathJax && MathJax.typesetPromise) {
128 MathJax.texReset(); // Reset Math counters
129 MathJax.typesetPromise(arg); // MathJax v3
130 }
131 } catch (err) {console.log(err);}
132 }
133 var ajaxSubmit;
134 function replAddNews(newtable) {
135 let newids = [], idlist=[];
136 let getArticleID = function (td) {
137 return parseInt(td.parentNode.getElementsByTagName("td")[1].id);
138 }
139 for (let i of newtable.querySelectorAll("td.repl"))
140 newids.push(i);
141 newids = newids.sort((a,b)=> {
142 return (getArticleID(a) - getArticleID(b));
143 });
144 for (i of newids)
145 idlist.push(getArticleID(i));
146 console.log("IDList="+idlist.join());
147 let cnt=0, ntr;
148 let current = collectElementsByAttr("td", "class", "repl"),
149 ncur=0, n, icur=0, o, oid, nid, otr;
150 current = document.querySelectorAll('td[class="repl"]');
151 let last=current[current.length-1],
152 tbody = last.parentNode.parentNode;
153 let addEventsToNewTr = function(tr) {
154 let td = tr.getElementsByTagName("td"),
155 td0 = td[0], td1 = td[1];
156 td0.classList.add("new");
157 registInsertDirect(td0.querySelectorAll("a[href]"));
158 registPjaxViewers(td1.querySelectorAll("a[href]"));
159 }
160 // Erase all "new article" flags before merging
161 for (let i of document.querySelectorAll("td.new"))
162 i.classList.remove("new");
163 // Now reconstruct articles with merge-sort like method
164 outer: for (; ncur<newids.length; ncur++) {
165 n = newids[ncur];
166 if (!n.id) continue;
167 nid = parseInt(n.id);
168 if (nid<=0) continue;
169 ntr = n.parentNode;
170 for (; icur<current.length; icur++) {
171 o = current[icur];
172 otr = o.parentNode;
173 oid = getArticleID(o);
174 if (!oid || oid=="") continue;
175 if (oid >= nid) {
176 addEventsToNewTr(ntr);
177 tbody.insertBefore(ntr, otr);
178 if (oid==nid) otr.remove();
179 cnt++;
180 continue outer;
181 }
182 }
183 // Append absolutely new articles.
184 ntr = n.parentNode;
185 addEventsToNewTr(ntr)
186 tbody.appendChild(atMarkView(ntr));
187 ntr.classList.add("dissolving");
188 let localntr = ntr;
189 setTimeout(() => {
190 localntr.classList.remove("dissolving");
191 localntr.classList.add("emerging");
192 }, 100);
193 rewriteReplyHover(ntr);
194 cnt++;
195 }
196 mathjaxUpdate(newids);
197 console.log("Update "+cnt+"rows");
198 if (cnt>0 && ntr.scrollIntoView) {
199 let option = {behavior: "smooth"};
200 if (!isOlderJS) option.block = "center";
201 try { // Scroll to last updated row
202 ntr.scrollIntoView(option);
203 } catch (e1) {}
204 }
205 return cnt;
206 }
208 function warnFileSize(form) {
209 let szmax = form.querySelector('input[name="filesize_max"]').value;
210 if (!szmax || szmax=="") return;
211 szmax = parseInt(szmax);
212 if (szmax <= 0) return;
213 // szmax = 10000
214 let ng = "", rcval=false, fileexists=false,
215 pdfsw = form.querySelector(input_pdfsw),
216 pdfmsg = "Try compressing PDF?\nPDFを圧縮してみますか?\n" +
217 "(それでも収まらない場合もあります)";
218 for (let f of form.querySelectorAll('input[type="file"]')) {
219 let thiserr = false;
220 for (let i of f.files) {
221 fileexists = true;
222 let fn = i.name, sz = i.size;
223 console.log("max="+szmax+", fn="+fn+", sz="+sz);
224 if (sz > szmax) {
225 if (fn.match(/\.pdf/i)
226 && sz < szmax*3 // XXX : x3 reasonable?
227 && (pdfsw || confirm(pdfmsg))) {
228 if (!pdfsw) {
229 pdfsw = document.createElement("input");
230 pdfsw.name = "comppdf";
231 pdfsw.type = "hidden";
232 f.parentNode.insertBefore(pdfsw, f);
233 pdfsw.value = "yes";
234 }
235 } else {
236 thiserr = true;
237 ng += ((ng>"" ? ", " : "")+fn)
238 }
239 }
240 }
241 thiserr ? f.classList.add("warnbg") : f.classList.remove("warnbg");
242 }
243 if (ng>"") {
244 rcval = "File-size Limit Error: "+ng+"\n"+
245 "Should be less than "+szmax+"bytes.\n"+
246 szmax+"バイト未満にしてください"
247 alert(rcval);
248 }
249 if (form.text.value == "") {
250 let w;
251 if (fileexists)
252 w = "Fill the text area\n" +
253 "添付したファイルに関する説明を入れてください。";
254 else
255 w = "Enter your comment!\n何か書き込んでね!";
256 alert(w);
257 rcval = (rcval || w);
258 form.text.classList.add("warnbg");
259 setTimeout(() => {form.text.classList.remove("warnbg");}, 2000)
260 }
261 return rcval;
262 }
263 function ajaxPost(e) {
264 e.preventDefault();
265 let rowid;
266 if (!myurl.match(/replyblog\+([0-9]+)/)) return;
267 rowid = RegExp.$1
268 let myform = document.querySelector("form.replyblog");
269 let data = new FormData(myform),
270 fetchtime = data.get("fetchtime");
271 if (!fetchtime || fetchtime=="") return;
272 ///*XX*/fetchtime = "2020-06-14T00:00:00";data.set("fetchtime", fetchtime)
274 ajaxSubmit = e.target;
275 ajaxSubmit.back = ajaxSubmit.textContent;
276 if (ajaxSubmit.id == "reload") {
277 ajaxSubmit.textContent = "更新中"
278 data.set("text", "")
279 } else {
280 if (warnFileSize(myform)) return;
281 ajaxSubmit.textContent = "送信中";
282 }
283 ajaxSubmit.blur();
284 ajaxSubmit.disabled = true;
285 let act = mypath+"?blog_fetch+"+rowid+"+f:"+fetchtime;
287 function respUpdate(tbody) {
288 ajaxSubmit.textContent = ajaxSubmit.back;
289 ajaxSubmit.disabled = false;
290 let div = document.createElement("div"), form, newform;
291 try {
292 div.innerHTML = tbody;
293 form = div.querySelector("form");
294 } catch (er) {
295 alert("Cannot parse fetch data");
296 return;
297 }
298 let update = replAddNews(form);
299 let dispelem = myform.querySelector("textarea").parentNode;
300 if (div.querySelector('input[name="user"]')) { // is login form
301 dispInfoMomentary("Login Again Please", dispelem)
302 return;
303 }
304 newform = new FormData(form);
305 if (data.get("text") > "") { // Called by submit button
306 myform.reset();
307 let pdfsw = myform.querySelector(input_pdfsw);
308 if (pdfsw) pdfsw.remove();
309 // myform.text.value = '';
310 }
311 myform.fetchtime.value = newform.get("fetchtime");
312 myform.id.value = newform.get("id");
313 if (update && update > 0) {
314 let s = update + " new article" +
315 (update>1 ? "s" : "") + " posted";
316 dispInfoMomentary(s, dispelem);
317 }
318 }
319 fetch(act, {
320 method: "POST", body: data,
321 credentials: "include" // For older firefox
322 }).then((resp) => {
323 return resp.text();
324 }).then((tbody) => {
325 respUpdate(tbody);
326 })
327 }
328 function pjaxView(ev, url, mynum) {
329 if (ev.ctrlKey||ev.shiftKey) return;
330 ev.preventDefault();
331 let box = document.createElement("div")
332 box.setAttribute("class", "pjaxview");
333 let p1 = document.createElement("p"),
334 bt = document.createElement("button"),
335 sl = document.createElement("button"),
336 sr = document.createElement("button"),
337 loading = document.createElement("span"),
338 info = document.createElement("p");
339 info1 = document.createElement("span");
340 info2 = document.createElement("span");
341 iframe = document.createElement("iframe");
342 var curpos = mynum;
343 var historyBase = history.length;
345 function _setPjaxCurposInfo() {
346 let len = art_m_list.length;
347 let cur = art_m_list[curpos]
348 info1.textContent = (1+curpos)+" of "+len+" article #"+cur.id+
349 (cur.author ? " by "+cur.author : "") + ":";
350 info2.textContent = cur.text.trim();
351 info2.setAttribute("class", "border textdigest");
352 }
353 function _resetPjax() {
354 // All we can do surely is to back 1 page,
355 // because we cannot move to desirable entry of history list.
356 history.back();
357 }
358 function setSwipeAct(iframe) {
359 // We cannot use DOMContentLoaded nor iframe.contentWindow here.
360 // PDF.js does not construct contentWindow...?
361 iframe.addEventListener("load", () => {
362 loading.classList.remove("loading");
363 if (!hasTouchPad) return;
364 let ifm = iframe.contentDocument;
365 let startX, moveX, thresh = 100;
366 ifm.addEventListener("touchstart", (e) => {
367 e.preventDefault();
368 startX = e.touches[0].pageX;
369 }, false);
370 ifm.addEventListener("touchmove", (e) => {
371 e.preventDefault();
372 moveX = e.touches[0].pageX;
373 }, false);
374 ifm.addEventListener("touchend", (e) => {
375 if (startX < moveX && startX + thresh < moveX) {
376 switchTo(e, -1);
377 } else if (startX > moveX && startX - thresh > moveX) {
378 switchTo(e, +1);
379 }
380 }, false);
381 }, false);
383 }
384 function switchTo(e, direction) {
385 e.preventDefault();
386 let len = art_m_list.length, cur, newpos, url;
387 newpos = (curpos+len+direction)%len;
388 if (curpos == newpos) return; // No need to switch to same one
389 curpos = newpos;
390 cur = art_m_list[curpos];
391 url = cur.url;
392 // We should remove iframe once to preserve history Object
393 // https://inthetechpit.com/2019/04/20/update-iframe-without-affecting-browser-history/
394 let parent = iframe.parentNode;
395 // alert("D = "+direction);
396 iframe.remove();
397 parent.appendChild(iframe);
398 try {
399 loading.classList.add("loading");
400 iframe.src = url;
401 // iframe.contentDocument.location.replace(url);
402 // location.replace cannot be used because PDF viewer.js
403 // does not have iframe.contentDocument
404 } catch (err) {
405 alert("Cannot load "+src+" : "+err.name);
406 }
407 _setPjaxCurposInfo();
408 setSwipeAct(iframe);
409 }
410 function switchToByKey(e) {
411 // alert("KEY="+e.key);
412 switch (e.key) {
413 case "ArrowLeft":
414 switchTo(e, -1); break;
415 case "ArrowRight":
416 switchTo(e, +1); break;
417 case "Escape":
418 history.back();
419 }
420 }
421 // <div><p>
422 // <button> << </button><button>Dismiss</button><button> >> </button>
423 // </p><p><span> info1 </span> <span> info2 </span></p>
424 // <iframe src="..."></iframe>
425 // </div>
426 // ==> [ << ][Dissmiss][ >> ]
427 // ==> ## of ## article #xxx by AUTHOR
428 sl.textContent = " << ";
429 sr.textContent = " >> ";
430 sl.addEventListener("click", (e) => {switchTo(e, -1);});
431 sr.addEventListener("click", (e) => {switchTo(e, +1);});
432 sl.setAttribute("title", "to="+(mynum-1));
433 sr.setAttribute("title", "to="+(mynum+1));
434 document.body.appendChild(box);
435 bt.textContent = "Click to dismiss / もどる"+mynum;
437 box.appendChild(p1);
438 p1.appendChild(sl); p1.appendChild(bt); p1.appendChild(sr);
439 { // TEST: Normal mode
440 let only = document.createElement("button"),
441 h = location.href;
442 only.textContent = ".oO□";
443 only.setAttribute("title", "Open in Normal Window");
444 only.addEventListener("click", function() {
445 location.replace(iframe.src);
446 });
447 p1.appendChild(only);
448 }
449 p1.appendChild(loading);
450 info.appendChild(info1); info.appendChild(info2);
451 loading.textContent=" Loading...";
452 loading.classList.add("hidden");
453 loading.classList.add("loading");
454 box.appendChild(info);
455 iframe.src = url;
457 box.addEventListener("keydown", switchToByKey);
458 //box.addEventListener("click", (e) => {_resetPjax();});
459 bt.addEventListener("click", (e) => {_resetPjax();});
460 // dp.addEventListener("click", (e) => {_resetPjax();});
461 info.addEventListener("click", (e) => {_resetPjax();});
462 box.appendChild(iframe);
464 setSwipeAct(iframe);
466 _setPjaxCurposInfo();
467 bt.focus();
468 setTimeout(() => {box.classList.add("pjaxview2");}, 10);
469 // Finally update history stack
470 pjaxHistoryPush(box);
471 }
472 function pjaxHistoryPush(box) {
473 if (history.pushState) {
474 let h = location.href.replace(/#.*/, '')+"#pjaxview";
475 history.pushState({url: h}, null, h);
476 window.addEventListener("popstate", (e) => {
477 if (box) {
478 box.remove(); box = null;
479 }
480 }, false);
481 }
482 }
483 function reverseChecks() {
484 var names = collectElementsByAttr("input", "name", "usel");
485 for (let u of names) {
486 u.checked = !u.checked;
487 }
488 }
489 function renumberOL(str, start) {
490 var stra = str.split("\n");
491 for (var i=1; i<stra.length; i++) {
492 if (stra[i].match(/^[1-9][0-9]*\. /)) {
493 let orig=stra[i];
494 stra[i] = (++start)+". "+RegExp.rightContext;
495 } else if (stra[i].match(/^ /)) {
496 continue;
497 } else
498 break;
499 }
500 return stra.join("\n");
501 }
502 function submitThisForm(e) {
503 var input = e.target, ajaxpost = document.getElementById("c");
504 for (var elm=input.parentNode; elm; elm = elm.parentNode) {
505 if (ajaxpost) {
506 ajaxpost.click();
507 return true;
508 } else if (elm.nodeName.match(/form/i)) {
509 elm.submit();
510 return true;
511 }
512 }
513 return false;
514 }
515 function helpMarkdownBS(e) {
516 var area = e.target, pos = area.selectionStart, text = area.value;
517 if (area.selectionStart != area.selectionEnd) return;
518 if (pos<2) return;
519 if (text.substr(pos-1, 2)=="\n\n") return;
520 var bol = text.lastIndexOf("\n", pos-1),
521 eol = text.indexOf("\n", pos);
522 if (bol<=0 || bol==eol) return;
523 var thisline = text.substring(bol+1, eol==-1 ? text.length : eol);
524 thisline = text.substring(bol+1, pos);
525 if (thisline == "* ") {
526 area.setSelectionRange(pos-2, pos);
527 } else if (thisline.match(/^[1-9][0-9]*\. $/)) {
528 area.setSelectionRange(pos-RegExp.lastMatch.length, pos);
529 }
530 }
531 function helpMarkdownEnter(e) {
532 if (e.keyCode == 13 && !e.shiftKey) {
533 if (e.ctrlKey && submitThisForm(e)) {
534 e.preventDefault();
535 return;
536 }
537 var area = e.target;
538 var pos = area.selectionStart, text = area.value;
539 if (pos==0) return;
540 var last = text.lastIndexOf("\n", pos-1);
541 var rest = text.substring(pos), rest0=rest;
542 var line = last ? text.substring(last+1, pos) : text;
543 var next = rest.substring(rest.indexOf("\n"))||rest;
544 next=next.substring(1);
545 var tail = text.substring(pos-2, pos), br = (tail==" ");
546 var add = "", offset = 1;
547 if (line.startsWith("* ")) {
548 add = "* ";
549 offset += add.length;
550 if (br) {
551 add = " " + "\n" + add;
552 }
553 } else if (line.match(/^([1-9][0-9]*)\. /)) {
554 var ln = parseInt(RegExp.$1), nn=ln+1,
555 len = RegExp.lastMatch.length;
556 add = nn+". ";
557 let toeol = text.substr(pos, text.indexOf("\n"));
558 if (br) {
559 if (next.startsWith(add)) {
560 add=" ".repeat(len);
561 nn = ln;
562 } else {
563 add = " ".repeat(len)+ "\n" + add;
564 offset -= len+1;
565 }
566 }
567 if (next.match(/^[1-9][0-9]*\. /))
568 rest = renumberOL(rest, nn);
569 offset += add.length;
570 } else if (line.match(/^\|( *).+\|/)) {
571 add = "|" + RegExp.$1 + " |";
572 offset += add.length-2;
573 } else {
574 return;
575 }
576 e.preventDefault();
577 if (!document.execCommand("insertText", false, "\n"+add)) {
578 //Firefox
579 area.selectionEnd = area.value.length;
580 area.setRangeText("\n"+add+rest);
581 area.selectionEnd = null;
582 } else {
583 area.selectionEnd = area.value.length;
584 area.setSelectionRange(area.selectionStart, area.value.length);
585 document.execCommand("insertText", false, rest);
586 area.selectionEnd = null;
587 area.focus();
588 }
589 area.selectionStart = pos+offset;
590 return;
591 if (document.execCommand("insertText", false, "\n"+add)) {
592 //area.setSelectionRange(area.selectionStart, text.length);
593 // alert("rest=["+rest+"], add=["+add+"]");
594 alert(text.substring(pos, area.value.length));
595 if (rest != rest0) {
596 area.setSelectionRange(pos, area.value.length);
597 return;
598 document.execCommand("delete");
599 }
600 document.execCommand("insertText", false, rest);
601 } else {
602 // Firefox cannot use insertText in textarea...
603 area.value = text.substring(0, pos) + "\n" + add + rest;
604 }
605 //area.setSelectionRange(pos+length(add));
606 area.selectionStart=area.selectionEnd = (pos + offset);
608 }
609 }
610 var helpParenPreview = 0;
611 function helpMarkdownParen(e) {
612 if (!mathjax) return;
613 var area = e.target, pos = area.selectionStart, text = area.value;
614 if (pos<2) return;
615 if (text[pos-1] == "\\") {
616 let ins="( \\)";
617 if (text[pos-2] == "\\") ins="( \\\\)";
618 area.setRangeText(ins, pos, pos);
619 area.selectionStart = pos+2;
620 if (helpParenPreview++ < 1) {
621 dispInfoMomentary("Preview formula by Meta-p\n"+
622 "Meta-p で数式プレビュー", e.target.parentNode);
623 }
624 e.preventDefault();
625 }
626 }
627 function textInsert(area, string, pos1, pos2) {
628 console.log("str="+string);
629 area.setRangeText(string, pos1||area.selectionStart,
630 pos2||pos1||area.selectionStart);
631 area.selectionStart += string.length;
632 }
633 function beginningOfLine(area, pos) {
634 pos = pos||area.selectionStart;
635 let b = area.value.lastIndexOf("\n", pos);
636 if (pos>1 && area.value.charCodeAt(pos)==10)
637 b = area.value.lastIndexOf("\n", pos-1);;
638 return b>=0 ? b : 0;
639 }
640 function isInBeginEnd(area, pos){
641 pos = pos||area.selectionStart;
642 let bol = beginningOfLine(area, pos);
643 let thisline = area.value.substr(bol);
644 console.log("curchar="+area.value.charCodeAt(pos));
645 console.log("prechar="+area.value.charCodeAt(pos-1));
646 console.log("bol="+bol+", thisline="+thisline);
647 let match = thisline.search(/\\(begin|end){([A-Za-z]*)/), lm, be;
648 if (match >= 0) {
649 lm = RegExp.lastMatch;
650 be = RegExp.$1;
651 return RegExp.$2
652 }
653 return null;
654 }
655 function helpMarkdownBrace(e) {
656 if (!mathjax) return;
657 var area = e.target, pos = area.selectionStart, text = area.value,
658 begin = "\\begin", end = "\\end";
659 if (pos < end.length) return;
660 if (text.substr(pos-end.length).startsWith(end)) {
661 let beg = text.lastIndexOf(begin, pos);
662 if (beg >= 0) {
663 let env = text.substr(beg).search(/\\begin{(.*?)}/);
664 if (env >= 0) {
665 textInsert(area, "{"+RegExp.$1+"}", pos);
666 e.preventDefault();
667 }
668 }
669 }
670 }
671 function helpMarkdownBraceClose(e) {
672 if (!mathjax) return;
673 let area = e.target, pos = area.selectionStart, text = area.value,
674 begin = "\\begin", end = "\\end";
675 if (text.substr(pos).startsWith("}")) {
676 area.setRangeText("", pos, pos+1);
677 // e.preventDefault();
678 }
679 let inbegend = isInBeginEnd(area, pos);
680 if (!inbegend) return;
681 let nextendpos = text.substr(pos).indexOf("\\end{");
682 let nextcurend = text.substr(pos).indexOf("\\end{"+inbegend+"}");
683 if (nextcurend<0 || nextendpos!=nextcurend) {
684 area.setRangeText("}\n\n\\end{"+inbegend+"}", pos, pos);
685 area.selectionStart = pos+2;
686 e.preventDefault();
687 }
688 console.log(inbegend);
690 }
691 function helpMarkdownPreview(area) {
692 if (!mathjax) {
693 alert("no"+e.target)
694 dispInfoMomentary("This board has no MathJax mode.\n"+
695 "この掲示板は数式モードOFFです。",
696 e.target.parentNode);
697 return;
698 }
699 let text = area.value;
700 let preview = document.createElement("div");
701 let bp = document.createElement("p");
702 let btn = document.createElement("button");
703 btn.innerText = "Click or ESC to Dissmiss / クリックかESCで戻る";
704 bp.classList.add("c");
705 preview.classList.add("pjaxview");
706 preview.classList.add("pjaxview2");
707 let pre = document.createElement("p");
708 bp.appendChild(btn);
709 preview.appendChild(bp);
710 preview.appendChild(pre);
711 pre.innerText = text;
712 document.body.appendChild(preview);
713 function dismiss(t) {
714 history.back();
715 preview.remove();
716 area.focus();
717 }
718 preview.addEventListener("click", dismiss, false);
719 preview.addEventListener("keydown", dismiss, false);
720 MathJax.typesetPromise([pre]);
721 pjaxHistoryPush(preview);
722 btn.focus();
723 }
724 function helpMarkdownAt(e) {
725 var area = e.target, pos = area.selectionStart;
726 if (pos == 0) {
727 area.value = "@all" + area.value;
728 area.selectionStart = area.selectionEnd = 4;
729 dispInfoMomentary("@all で全員に通知します", area.parentNode);
730 e.preventDefault();
731 }
732 }
733 function helpMarkdown(e) {
734 switch (e.key) {
735 case "Backspace": helpMarkdownBS(e); break;
736 case "Enter": helpMarkdownEnter(e); break;
737 case "(": helpMarkdownParen(e); break;
738 case "p": if (e.metaKey) helpMarkdownPreview(e.target); break;
739 case "{": helpMarkdownBrace(e); break;
740 case "}": helpMarkdownBraceClose(e); break;
741 case "@": helpMarkdownAt(e); break;
742 }
743 }
744 /* Init event listeners */
745 function addFileInput() {
746 var inpfile = collectElementsByAttr("input", "name", "image");
747 if (!inpfile) return;
748 var filled = true;
749 var i, ih;
750 for (i of inpfile) {
751 if (! i.value) filled=false;
752 }
753 if (filled) {
754 ih = i.parentNode.innerHTML;
755 let ipph = i.parentNode.parentNode.innerHTML; //Entire td inside
756 if (ipph) {
757 let ip = i.parentNode;
758 var inpf = ipph.substring(ipph.indexOf('<span class="file')),
759 newi = "<br>"+inpf.substring(0, inpf.indexOf("/span>")+6);
760 ip.insertAdjacentHTML("afterend", newi)
761 ip.nextSibling.nextSibling.addEventListener('change', () => {
762 warnFileSize(document.forms[0]);
763 });
764 }
765 }
766 }
767 function initFileInput() { // Multiplies "input type=file"
768 var el, morefile = document.getElementById("morefile");
769 if (morefile) {
770 for (el of collectElementsByAttr("input", "name", "image")) {
771 el.addEventListener("change", function(ev) {
772 if (ev.target.value > "" && ev.target.files.length == 1)
773 morefile.style.visibility = "visible";
774 // No need to hide again, sure?
775 });
776 }
777 morefile.addEventListener("click", addFileInput, null);
778 }
779 // When renaming, select basename part
780 for (el of collectElementsByAttr("input", "class", "mv")) {
781 el.addEventListener("focus", function(ev) {
782 var i = ev.target;
783 if (i) {
784 i.setSelectionRange(0, i.value.lastIndexOf("."));
785 }
786 });
787 }
788 }
789 function initTextarea() {
790 var te = collectElementsByAttr("textarea", "name", "text");
791 if (!te || !te[0]) return;
792 te[0].addEventListener("keydown", helpMarkdown, false);
793 }
794 function atMarkView(elem) {
795 // Enclose "@all" with span
796 for (i of elem.querySelectorAll("td.repl")) {
797 if (i.textContent.startsWith("@all")) {
798 i.firstChild.nodeValue = i.firstChild.nodeValue.substring(4);
799 i.insertAdjacentHTML(
800 'afterbegin', '<div class="atall">@all</div>'
801 );
802 i.classList.add("atall");
803 }
804 }
805 return elem;
806 }
807 var authVisible = false;
808 function toggleAuthorVisibility(e) {
809 // Click to hideauth button toggles visibility of author columns
810 e.stopPropagation();
811 let b = document.getElementById("hideauth");
812 if (authVisible) {
813 for (let i of document.querySelectorAll("td.repatt")) {
814 i.classList.remove("hideauthor");
815 }
816 authVisible = false;
817 } else {
818 for (let i of document.querySelectorAll("td.repatt")) {
819 i.classList.add("hideauthor");
820 }
821 authVisible = true;
822 }
823 b.textContent = b.textContent.replace(/OFF|ON/, authVisible ? "ON" : "OFF");
824 }
825 function downloadFile(filename, content, BOM) {
826 let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
827 let str = new Blob(BOM ? [bom, content] : [content],
828 {type: "text/csv"});
829 var uri = URL.createObjectURL(str);
830 let a = document.createElement("a");
831 a.download = (BOM ? "BOM-" : "")+filename;
832 a.href = uri;
833 document.body.appendChild(a);
834 a.click();
835 document.body.removeChild(a);
836 }
837 function getTextContentCSV_1(e) {
838 let blogtbl = document.querySelector("table.blog_replies");
839 let needBOM = e.ctrlKey;
840 if (!blogtbl) return;
841 let trw = blogtbl.querySelector("tr.warn"), a;
842 if (trw && (a=trw.querySelector("th>a"))) {
843 if (a.title == "Show All") {
844 if (window.confirm(`50件以下に表示制限されています。
845 取得し直しますか?
846 Cancelを押すとこのまま取得します。
847 Seen articles limited to 50 items.
848 Push OK to get all articles, Cancel to get only seen articles.`)) {
849 a.click();
850 return;
851 }
852 }
853 }
854 if (navigator.userAgent.match(/Windows/)) {
855 if (!e.ctrlKey && !e.shiftKey && !window.confirm(`Excelで読ませるCSVの場合はBOMが必要です。
856 その場合は一度キャンセルして Ctrl キーを押しながらボタンクリックして下さい。
857 逆にExcel以外(GoogleスプレッドシートやLibreOfficeや他のツール)で読む場合はBOMをつけるとファイルの1行目の先頭にゴミのようなものが見える場合あるのでそのときは除去する必要があります。
858 今後もBOM不要の場合はShiftキーを押しながらクリックして下さい。
859 If you feed this CSV into Microsoft Excel, consider adding BOM sequence that can be prepended by pressing Control key with click.
860 In this case, click CSVget with ctrl key after Cancel this dialog.
861 If you never need BOM, press Shift key with click.`))
862 return;
863 }
864 outcsv = []
865 for (let row of blogtbl.querySelectorAll("tr[id]")) {
866 let tds = row.querySelectorAll("td"),
867 a = tds[0].querySelector("a.author"),
868 author = a.title,
869 name = a.innerText,
870 time = tds[0].querySelector("span").title,
871 id = tds[1].id,
872 body = tds[1].textContent;
873 //console.log(`${author},${name},${time},#${id},${body}`);
874 outcsv.push({
875 "author": author, "name": name, "time": time,
876 "id": "#"+id, "body": body});
877 }
878 let line = new CSV(outcsv, {header:true}).encode(),
879 fn = myurl.replace(/.*\?/, "").replaceAll("+", "-")
880 .replace(/#.*/, "").replace("-n:all", "");
881 downloadFile(fn+".csv", line, needBOM);
882 }
883 function getTextContentCSV(e) {
884 if (!document.getElementById("csvminjs")) {
885 let csvmin = document.createElement("script");
886 csvmin.src="https://www.yatex.org/libcache/csv.min.js";
887 csvmin.id = "csvminjs";
888 // https://stackoverflow.com/questions/14521108/dynamically-load-js-inside-js
889 csvmin.addEventListener("load", ()=>{
890 getTextContentCSV_1(e)}, 10);
891 document.querySelector("head").appendChild(csvmin);
892 } else {
893 getTextContentCSV_1(e);
894 }
895 }
896 function initBlogs() {
897 // Auto-complete #xxxx
898 let i, check = collectElementsByAttr("input", "name", "notifyto");
899 if (check)
900 for (i of check) {
901 i.addEventListener("click", insertRedirect, false);
902 }
903 registInsertDirect(document.querySelectorAll("a[href]"));
904 if (myurl.match(/replyblog\+[0-9]/)
905 && document.querySelector("td.repl")) {
906 // There's no need to provide ajax posting when
907 // no replies written to the blog. Therefore we
908 // assign ajax post when td.repl exists.
909 for (i of document.querySelectorAll('input#c[value="送信"]')) {
910 let b = document.createElement("button");
911 b.textContent = "送信!";
912 console.log("b="+b+", tc="+b.textContent);
913 b.addEventListener("click", ajaxPost, false);
914 // i.insertAdjacentElement('afterend', b);
915 b.setAttribute("class", i.getAttribute("class"))
916 b.setAttribute("title", i.getAttribute("title"))
917 i.parentNode.replaceChild(b, i);
918 b.id = i.id;
919 // i.remove();
920 i.classList.add("aux");
921 i.value = "送信(予備)"
922 b.parentNode.appendChild(i);
923 }
924 i = document.getElementById("reload");
925 if (i) i.addEventListener("click", ajaxPost, false);
926 // Add CSV download button
927 let td = document.querySelector("table.bloghead tr td");
928 if (td) {
929 let btn = document.createElement("button");
930 btn.innerText = "CSVget";
931 btn.type = "button";
932 btn.accessKey = "g";
933 btn.title = `Shortcut: G
934 見えている書き込みをCSVで取得します
935 全件表示されていることを確認してから利用して下さい。
936 Excelで利用する場合は Ctrl を押しながらクリックして下さい。
937 Get seen TEXT content as CSV.`;
938 btn.addEventListener("click", getTextContentCSV, false);
939 let artlink = td.querySelector('a[accesskey="f"]');
940 let spacer = document.createElement("span");
941 if (artlink) {
942 spacer.innerText = "|";
943 artlink.insertAdjacentElement('beforebegin', btn);
944 artlink.insertAdjacentElement('beforebegin', spacer);
945 } else {
946 spacer.innerText = " ";
947 td.appendChild(spacer);
948 td.appendChild(btn);
949 }
950 }
951 }
952 for (i of document.querySelectorAll('input[type="file"]')) {
953 i.addEventListener('change', (e) => {
954 warnFileSize(document.forms[0]);
955 }, false)
956 }
957 if (i=document.getElementById("hideauth")) {
958 i.addEventListener('click', toggleAuthorVisibility, false);
959 }
960 // Hack article_m links
961 registPjaxViewers(document.querySelectorAll("a[href]"));
962 atMarkView(document);
963 }
964 function initGrpAction() {
965 var rev = document.getElementById("reverse");
966 if (!rev) return; // Is not grpAction page
967 if (rev.tagName.match(/span/i)) {
968 rev.textContent = " 反転 ";
969 rev.addEventListener("click", reverseChecks, null);
970 }
971 var emailbtn = document.getElementById("email");
972 emailbtn.addEventListener("click", function(ev){
973 // Enlarge box and Select user's checkbox
974 if (!ev.target.checked) return;
975 var x = collectElementsByAttr("div", "class", "foldtabs");
976 if (x && x[0] && x[0].style) {
977 x[0].style.height = "10em";
978 }
979 let myuid = document.getElementById("myuid");
980 if (myuid) {
981 let usel = collectElementsByAttr("input", "name", "usel");
982 if (usel) {
983 for (u of usel) {
984 if (u.value == myuid.value)
985 u.checked = true;
986 }
987 }
988 }
989 }, null);
990 var teamsel = document.getElementById("selteam");
991 if (teamsel) {
992 var usel, p, team;
993 // Select all members of the team
994 teamsel.addEventListener("change", function(ev) {
995 var teamname = teamsel.value,
996 selected = new RegExp('(^| )'+teamname+"($|,)");
997 usel = collectElementsByAttr("input", "name", "usel");
998 if (!usel) return;
999 for (u of usel) {
1000 p = u.parentNode; // should be label
1001 if (!p) continue;
1002 if (teamname == "TEAM") { // Reset all checks
1003 u.checked = false; // when "TEAM" is selected
1004 } else {
1005 p = p.parentNode.parentNode;// should be tr
1006 team = nthChildOf(p, 5, "td")
1007 if (team && team.textContent
1008 && team.textContent.match(selected)) {
1009 u.checked = true;
1013 }, null);
1016 function dispInfoMomentary(msg, elem) {
1017 // Momentarily display MSG in tooltip-baloon relative to ELEM element.
1018 let help = document.createElement("p");
1019 elem.style.position = 'relative';
1020 elem.style.overflow = 'visible';
1021 help.setAttribute("class", "info-tooltip");
1022 help.innerHTML = msg;
1023 elem.appendChild(help);
1024 setTimeout(() => {
1025 help.classList.add("dissolving");
1026 setTimeout(() => help.remove(), 3000);
1027 }, 1000);
1029 function initGrphome() {
1030 console.log("initGrphome");
1031 // (1)Setup Frozen State Changing Button
1032 var ja = navigator.language.match(/ja/i);
1034 function toggleFrozen(e, rowid) {
1035 let tgt = mypath+"?blog_setfrozen+"+rowid;
1036 let td = e.target.parentNode;
1037 let tr = td.parentNode;
1038 fetch(tgt, {
1039 method: "POST",
1040 headers: {'Content-Type': 'text/html; charset=utf-8'},
1041 credentials: "include"
1042 }).then(function(resp) {
1043 return resp.text();
1044 }).then(function(tbody) {
1045 try {
1046 var json = JSON.parse(tbody);
1047 } catch (e) {
1048 return;
1050 let state = json.state, newstate, info;
1051 if (json.alert) {
1052 alert(json.alert)
1054 if (state.match(/frozen/i)) {
1055 newstate = "凍結";
1056 info = ja ? newstate+"に設定しました" : 'Set Frozen';
1057 } else {
1058 newstate = null;
1059 info = ja ? '稼動に設定しました' : 'Set Running';
1061 tr.setAttribute("class", newstate);
1062 dispInfoMomentary(info, td);
1063 });
1065 let btn = document.querySelectorAll("button.toggle-frozen");
1066 for (let b of btn) {
1067 let rowid = null;
1068 let td=b.parentNode, tr = td.parentNode, fr, ru;
1069 ru = ja ? "動" : "Running";
1070 fr = ja ? "凍" : "Frozen";
1071 b.setAttribute('frozen-marker', fr);
1072 b.setAttribute('running-marker', ru);
1073 for (let a of tr.querySelectorAll("a[href]")) {
1074 if (a.getAttribute("href").match(/\?replyblog\+([0-9]+)/)) {
1075 rowid = parseInt(RegExp.$1);
1076 break;
1079 if (rowid && rowid>0) {
1080 b.addEventListener("click", function(e) {
1081 if (!btn) return;
1082 toggleFrozen(e, rowid);
1083 }, false);
1084 b.setAttribute("title", "稼動/凍結をその場で切り替えます\n\
1085 Toggle Running/Frozen ("+rowid+")");
1088 // (2)Setup Column Collapse Button
1089 // INCOMPLETE: Cannot restore original state, but it's enough...
1090 function toggleColmnWidth(th) {
1091 let tbl = document.querySelector("table.dumpblogs");
1092 let colname = th.textContent, newwidth;
1093 if (th.style.width) {
1094 newwidth = null
1095 // https://developer.mozilla.org/ja/docs/Web/CSS/table-layout
1096 tbl.style.tableLayout = 'auto';
1097 tbl.style.width = null;
1098 } else {
1099 newwidth = "2em";
1100 tbl.style.tableLayout = 'fixed';
1101 tbl.style.width = '100%';
1103 th.style.width = newwidth;
1104 th.style.overflow = "hidden";
1105 for (let td of document.querySelectorAll("td."+colname)) {
1106 console.log(td.tagName);
1107 td.style.width = newwidth;
1108 console.log(td.style.width);
1111 let row1 = document.querySelector("table.dumpblogs tr:first-child");
1112 if (row1) {
1113 let heads = row1.querySelectorAll("th");
1114 for (let h of heads) {
1115 h.addEventListener("click", function(e) {
1116 toggleColmnWidth(h);
1117 }, false);
1118 h.setAttribute("title", "Click to shrink these columns");
1122 function initMath() {
1123 mathjax = window.MathJax||document.getElementById("mathjax");
1124 if (!mathjax) return;
1125 let ta = document.querySelector("textarea");
1126 if (!ta) return;
1127 let btn = document.createElement("button");
1128 btn.setAttribute("title", "\\( と \\) で数式利用\n"+
1129 "\\[ と \\] で段組み数式モード\n"+
1130 "便利なマクロ:\n"+
1131 " \\boxed{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1132 " \\underline{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1133 "独自定義マクロ:\n"+
1134 " \\warn{xxx} 注意喚起用色付き枠\n"+
1135 " \\Warn{xxx} 大きな文字で注意喚起")
1136 btn.innerHTML = "MathJax<br>Preview";
1137 btn.addEventListener('click', (e) => {
1138 e.preventDefault();
1139 ta.focus();
1140 helpMarkdownPreview(ta);
1141 });
1142 ta.parentNode.appendChild(btn);
1144 function rewriteReplyHover(unit) {
1145 function getTextById(id) {
1146 let repltd = document.getElementById(id);
1147 if (repltd) {
1148 let txt = repltd.innerText,
1149 authtd = repltd.parentNode.getElementsByTagName("td")[0],
1150 author = authtd.querySelector("a.author").innerText,
1151 digest = txt.split("\n").splice(0, hoverTextLines).join("\n");
1152 return escapeChars("[[ "+author+" ]]\n"+digest);
1153 } else
1154 return "";
1156 unit = unit||document;
1157 for (let td of unit.querySelectorAll("td.repl")) {
1158 let firstC = td.firstChild;
1159 // Direct replacing innerHTML breaks embedded DOM event handlers.
1160 // So, we split td.repl into elements and replace the first
1161 // textNode(nodeType==3) with hover-text embeded content.
1162 if (firstC.nodeType==3 && firstC.nodeValue.startsWith(">#")) {
1163 let newline = firstC.nodeValue.indexOf("\n");
1164 let firstline;
1165 if (newline > 0) {
1166 firstline = firstC.nodeValue.substring(0, 1+newline);
1167 firstC.nodeValue = firstC.nodeValue.substring(1+newline);
1168 } else {
1169 // Cannot be reached here, but leave this for robustness
1170 firstline = firstC.nodeValue;
1171 firstC.nodeValue = "";
1173 td.insertAdjacentHTML(
1174 'afterbegin',
1175 escapeChars(firstline).replace(
1176 /#([0-9]+)/g,
1177 (match, start, whole) => {
1178 let id = RegExp.$1
1179 return '<a title="' + getTextById(id)
1180 + '" href="' + match
1181 + '">' + match + '</a>';
1182 }));
1186 function initReplyHover(unit) {
1187 // https://stackoverflow.com/questions/60154233/event-when-typesetting-is-done-mathjax-3
1188 if (mathjax && MathJax.startup)
1189 MathJax.startup.promise.then(()=>rewriteReplyHover());
1190 else
1191 rewriteReplyHover();
1193 var getHandoutCSV;
1194 function initGetHandoutCSV() {
1195 getHandoutCSV = document.getElementById("gethandoutcsv");
1196 if (!getHandoutCSV) return;
1197 document.getElementById("bommsg").innerText =
1198 `Excelで開く場合は上記CSVリンクをCtrlを押しながら。
1199 You need to click CSV link above with Ctrl-key when you handle CSV with Excel`;
1200 getHandoutCSV.addEventListener("click", (e) => {
1201 e.preventDefault(); // Stop visiting link
1202 let bom = e.ctrlKey;
1203 let csv = document.getElementById("totalcsv");
1204 if (!csv || !csv.textContent) return;
1205 let fn = "report-count", plus=myurl.lastIndexOf("+");
1206 if (plus) fn += ("-"+myurl.substring(1+plus));
1207 fn = fn.replace(/#.*/, "");
1208 downloadFile(fn+".csv", csv.textContent, bom);
1209 });
1211 function init() {
1212 isOlderJS = !("insertAdjacentElement" in document.body);
1213 initGrpAction();
1214 initBlogs();
1215 initFileInput();
1216 initTextarea();
1217 initGrphome();
1218 initMath();
1219 initReplyHover();
1220 initGetHandoutCSV();
1222 document.addEventListener('DOMContentLoaded', init, null);
1223 })();