s4

view s4-main.js @ 1001:bbd5a0c50d5b

Keep pjaxview in @all post
author HIROSE Yuuji <yuuji@gentei.org>
date Sun, 04 Dec 2022 10:36:33 +0859
parents ddf85e80f64e
children 47b3e770372d
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 if (ih) {
756 var inpf = ih.substring(ih.indexOf("<input")),
757 newi = "<br>"+inpf.substring(0, inpf.indexOf(">")+1);
758 i.insertAdjacentHTML("afterend", newi)
759 i.nextSibling.nextSibling.addEventListener('change', () => {
760 // next==br next.next==input[type=file]
761 warnFileSize(document.forms[0]);
762 });
763 }
764 }
765 }
766 function initFileInput() { // Multiplies "input type=file"
767 var el, morefile = document.getElementById("morefile");
768 if (morefile) {
769 for (el of collectElementsByAttr("input", "name", "image")) {
770 el.addEventListener("change", function(ev) {
771 if (ev.target.value > "" && ev.target.files.length == 1)
772 morefile.style.visibility = "visible";
773 // No need to hide again, sure?
774 });
775 }
776 morefile.addEventListener("click", addFileInput, null);
777 }
778 // When renaming, select basename part
779 for (el of collectElementsByAttr("input", "class", "mv")) {
780 el.addEventListener("focus", function(ev) {
781 var i = ev.target;
782 if (i) {
783 i.setSelectionRange(0, i.value.lastIndexOf("."));
784 }
785 });
786 }
787 }
788 function initTextarea() {
789 var te = collectElementsByAttr("textarea", "name", "text");
790 if (!te || !te[0]) return;
791 te[0].addEventListener("keydown", helpMarkdown, false);
792 }
793 function atMarkView(elem) {
794 // Enclose "@all" with span
795 for (i of elem.querySelectorAll("td.repl")) {
796 if (i.textContent.startsWith("@all")) {
797 i.firstChild.nodeValue = i.firstChild.nodeValue.substring(4);
798 i.insertAdjacentHTML(
799 'afterbegin', '<div class="atall">@all</div>'
800 );
801 i.classList.add("atall");
802 }
803 }
804 return elem;
805 }
806 var quizwarnVisible = false;
807 function toggleAuthorVisibility(e) {
808 // In QUIZ mode, click to quizwarn line toggles visibility of author
809 e.stopPropagation();
810 if (quizwarnVisible) {
811 for (let i of document.querySelectorAll("td.repatt")) {
812 i.classList.remove("hideauthor");
813 }
814 quizwarnVisible = false;
815 } else {
816 for (let i of document.querySelectorAll("td.repatt")) {
817 i.classList.add("hideauthor");
818 }
819 quizwarnVisible = true;
820 }
821 }
822 function downloadFile(filename, content, BOM) {
823 let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
824 let str = new Blob(BOM ? [bom, content] : [content],
825 {type: "text/csv"});
826 var uri = URL.createObjectURL(str);
827 let a = document.createElement("a");
828 a.download = (BOM ? "BOM-" : "")+filename;
829 a.href = uri;
830 document.body.appendChild(a);
831 a.click();
832 document.body.removeChild(a);
833 }
834 function getTextContentCSV_1(e) {
835 let blogtbl = document.querySelector("table.blog_replies");
836 let needBOM = e.ctrlKey;
837 if (!blogtbl) return;
838 let trw = blogtbl.querySelector("tr.warn"), a;
839 if (trw && (a=trw.querySelector("th>a"))) {
840 if (a.title == "Show All") {
841 if (window.confirm(`50件以下に表示制限されています。
842 取得し直しますか?
843 Cancelを押すとこのまま取得します。
844 Seen articles limited to 50 items.
845 Push OK to get all articles, Cancel to get only seen articles.`)) {
846 a.click();
847 return;
848 }
849 }
850 }
851 if (navigator.userAgent.match(/Windows/)) {
852 if (!e.ctrlKey && !e.shiftKey && !window.confirm(`Excelで読ませるCSVの場合はBOMが必要です。
853 その場合は一度キャンセルして Ctrl キーを押しながらボタンクリックして下さい。
854 逆にExcel以外(GoogleスプレッドシートやLibreOfficeや他のツール)で読む場合はBOMをつけるとファイルの1行目の先頭にゴミのようなものが見える場合あるのでそのときは除去する必要があります。
855 今後もBOM不要の場合はShiftキーを押しながらクリックして下さい。
856 If you feed this CSV into Microsoft Excel, consider adding BOM sequence that can be prepended by pressing Control key with click.
857 In this case, click CSVget with ctrl key after Cancel this dialog.
858 If you never need BOM, press Shift key with click.`))
859 return;
860 }
861 outcsv = []
862 for (let row of blogtbl.querySelectorAll("tr[id]")) {
863 let tds = row.querySelectorAll("td"),
864 a = tds[0].querySelector("a.author"),
865 author = a.title,
866 name = a.innerText,
867 time = tds[0].querySelector("span").title,
868 id = tds[1].id,
869 body = tds[1].textContent;
870 //console.log(`${author},${name},${time},#${id},${body}`);
871 outcsv.push({
872 "author": author, "name": name, "time": time,
873 "id": "#"+id, "body": body});
874 }
875 let line = new CSV(outcsv, {header:true}).encode(),
876 fn = myurl.replace(/.*\?/, "").replaceAll("+", "-")
877 .replace(/#.*/, "").replace("-n:all", "");
878 downloadFile(fn+".csv", line, needBOM);
879 }
880 function getTextContentCSV(e) {
881 if (!document.getElementById("csvminjs")) {
882 let csvmin = document.createElement("script");
883 csvmin.src="https://www.yatex.org/libcache/csv.min.js";
884 csvmin.id = "csvminjs";
885 // https://stackoverflow.com/questions/14521108/dynamically-load-js-inside-js
886 csvmin.addEventListener("load", ()=>{
887 getTextContentCSV_1(e)}, 10);
888 document.querySelector("head").appendChild(csvmin);
889 } else {
890 getTextContentCSV_1(e);
891 }
892 }
893 function initBlogs() {
894 // Auto-complete #xxxx
895 let i, check = collectElementsByAttr("input", "name", "notifyto");
896 if (check)
897 for (i of check) {
898 i.addEventListener("click", insertRedirect, false);
899 }
900 registInsertDirect(document.querySelectorAll("a[href]"));
901 if (myurl.match(/replyblog\+[0-9]/)
902 && document.querySelector("td.repl")) {
903 // There's no need to provide ajax posting when
904 // no replies written to the blog. Therefore we
905 // assign ajax post when td.repl exists.
906 for (i of document.querySelectorAll('input#c[value="送信"]')) {
907 let b = document.createElement("button");
908 b.textContent = "送信!";
909 console.log("b="+b+", tc="+b.textContent);
910 b.addEventListener("click", ajaxPost, false);
911 // i.insertAdjacentElement('afterend', b);
912 b.setAttribute("class", i.getAttribute("class"))
913 b.setAttribute("title", i.getAttribute("title"))
914 i.parentNode.replaceChild(b, i);
915 b.id = i.id;
916 // i.remove();
917 i.classList.add("aux");
918 i.value = "送信(予備)"
919 b.parentNode.appendChild(i);
920 }
921 i = document.getElementById("reload");
922 if (i) i.addEventListener("click", ajaxPost, false);
923 // Add CSV download button
924 let td = document.querySelector("table.bloghead tr td");
925 if (td) {
926 let btn = document.createElement("button");
927 btn.innerText = "CSVget";
928 btn.type = "button";
929 btn.title = `見えている書き込みをCSVで取得します
930 全件表示されていることを確認してから利用して下さい。
931 Excelで利用する場合は Ctrl を押しながらクリックして下さい。
932 Get seen TEXT content as CSV.`;
933 btn.addEventListener("click", getTextContentCSV, false);
934 let artlink = td.querySelector('a[accesskey="f"]');
935 let spacer = document.createElement("span");
936 if (artlink) {
937 spacer.innerText = "|";
938 artlink.insertAdjacentElement('beforebegin', btn);
939 artlink.insertAdjacentElement('beforebegin', spacer);
940 } else {
941 spacer.innerText = " ";
942 td.appendChild(spacer);
943 td.appendChild(btn);
944 }
945 }
946 }
947 for (i of document.querySelectorAll('input[type="file"]')) {
948 i.addEventListener('change', (e) => {
949 warnFileSize(document.forms[0]);
950 }, false)
951 }
952 if (i=document.getElementById("quizwarn")) {
953 i.addEventListener('click', toggleAuthorVisibility, false);
954 }
955 // Hack article_m links
956 registPjaxViewers(document.querySelectorAll("a[href]"));
957 atMarkView(document);
958 }
959 function initGrpAction() {
960 var rev = document.getElementById("reverse");
961 if (!rev) return; // Is not grpAction page
962 if (rev.tagName.match(/span/i)) {
963 rev.textContent = " 反転 ";
964 rev.addEventListener("click", reverseChecks, null);
965 }
966 var emailbtn = document.getElementById("email");
967 emailbtn.addEventListener("click", function(ev){
968 // Enlarge box and Select user's checkbox
969 if (!ev.target.checked) return;
970 var x = collectElementsByAttr("div", "class", "foldtabs");
971 if (x && x[0] && x[0].style) {
972 x[0].style.height = "10em";
973 }
974 let myuid = document.getElementById("myuid");
975 if (myuid) {
976 let usel = collectElementsByAttr("input", "name", "usel");
977 if (usel) {
978 for (u of usel) {
979 if (u.value == myuid.value)
980 u.checked = true;
981 }
982 }
983 }
984 }, null);
985 var teamsel = document.getElementById("selteam");
986 if (teamsel) {
987 var usel, p, team;
988 // Select all members of the team
989 teamsel.addEventListener("change", function(ev) {
990 var teamname = teamsel.value,
991 selected = new RegExp('(^| )'+teamname+"($|,)");
992 usel = collectElementsByAttr("input", "name", "usel");
993 if (!usel) return;
994 for (u of usel) {
995 p = u.parentNode; // should be label
996 if (!p) continue;
997 if (teamname == "TEAM") { // Reset all checks
998 u.checked = false; // when "TEAM" is selected
999 } else {
1000 p = p.parentNode.parentNode;// should be tr
1001 team = nthChildOf(p, 5, "td")
1002 if (team && team.textContent
1003 && team.textContent.match(selected)) {
1004 u.checked = true;
1008 }, null);
1011 function dispInfoMomentary(msg, elem) {
1012 // Momentarily display MSG in tooltip-baloon relative to ELEM element.
1013 let help = document.createElement("p");
1014 elem.style.position = 'relative';
1015 elem.style.overflow = 'visible';
1016 help.setAttribute("class", "info-tooltip");
1017 help.innerHTML = msg;
1018 elem.appendChild(help);
1019 setTimeout(() => {
1020 help.classList.add("dissolving");
1021 setTimeout(() => help.remove(), 3000);
1022 }, 1000);
1024 function initGrphome() {
1025 console.log("initGrphome");
1026 // (1)Setup Frozen State Changing Button
1027 var ja = navigator.language.match(/ja/i);
1029 function toggleFrozen(e, rowid) {
1030 let tgt = mypath+"?blog_setfrozen+"+rowid;
1031 let td = e.target.parentNode;
1032 let tr = td.parentNode;
1033 fetch(tgt, {
1034 method: "POST",
1035 headers: {'Content-Type': 'text/html; charset=utf-8'},
1036 credentials: "include"
1037 }).then(function(resp) {
1038 return resp.text();
1039 }).then(function(tbody) {
1040 try {
1041 var json = JSON.parse(tbody);
1042 } catch (e) {
1043 return;
1045 let state = json.state, newstate, info;
1046 if (json.alert) {
1047 alert(json.alert)
1049 if (state.match(/frozen/i)) {
1050 newstate = "凍結";
1051 info = ja ? newstate+"に設定しました" : 'Set Frozen';
1052 } else {
1053 newstate = null;
1054 info = ja ? '稼動に設定しました' : 'Set Running';
1056 tr.setAttribute("class", newstate);
1057 dispInfoMomentary(info, td);
1058 });
1060 let btn = document.querySelectorAll("button.toggle-frozen");
1061 for (let b of btn) {
1062 let rowid = null;
1063 let td=b.parentNode, tr = td.parentNode, fr, ru;
1064 ru = ja ? "動" : "Running";
1065 fr = ja ? "凍" : "Frozen";
1066 b.setAttribute('frozen-marker', fr);
1067 b.setAttribute('running-marker', ru);
1068 for (let a of tr.querySelectorAll("a[href]")) {
1069 if (a.getAttribute("href").match(/\?replyblog\+([0-9]+)/)) {
1070 rowid = parseInt(RegExp.$1);
1071 break;
1074 if (rowid && rowid>0) {
1075 b.addEventListener("click", function(e) {
1076 if (!btn) return;
1077 toggleFrozen(e, rowid);
1078 }, false);
1079 b.setAttribute("title", "稼動/凍結をその場で切り替えます\n\
1080 Toggle Running/Frozen ("+rowid+")");
1083 // (2)Setup Column Collapse Button
1084 // INCOMPLETE: Cannot restore original state, but it's enough...
1085 function toggleColmnWidth(th) {
1086 let tbl = document.querySelector("table.dumpblogs");
1087 let colname = th.textContent, newwidth;
1088 if (th.style.width) {
1089 newwidth = null
1090 // https://developer.mozilla.org/ja/docs/Web/CSS/table-layout
1091 tbl.style.tableLayout = 'auto';
1092 tbl.style.width = null;
1093 } else {
1094 newwidth = "2em";
1095 tbl.style.tableLayout = 'fixed';
1096 tbl.style.width = '100%';
1098 th.style.width = newwidth;
1099 th.style.overflow = "hidden";
1100 for (let td of document.querySelectorAll("td."+colname)) {
1101 console.log(td.tagName);
1102 td.style.width = newwidth;
1103 console.log(td.style.width);
1106 let row1 = document.querySelector("table.dumpblogs tr:first-child");
1107 if (row1) {
1108 let heads = row1.querySelectorAll("th");
1109 for (let h of heads) {
1110 h.addEventListener("click", function(e) {
1111 toggleColmnWidth(h);
1112 }, false);
1113 h.setAttribute("title", "Click to shrink these columns");
1117 function initMath() {
1118 mathjax = window.MathJax||document.getElementById("mathjax");
1119 if (!mathjax) return;
1120 let ta = document.querySelector("textarea");
1121 if (!ta) return;
1122 let btn = document.createElement("button");
1123 btn.setAttribute("title", "\\( と \\) で数式利用\n"+
1124 "\\[ と \\] で段組み数式モード\n"+
1125 "便利なマクロ:\n"+
1126 " \\boxed{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1127 " \\underline{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1128 "独自定義マクロ:\n"+
1129 " \\warn{xxx} 注意喚起用色付き枠\n"+
1130 " \\Warn{xxx} 大きな文字で注意喚起")
1131 btn.innerHTML = "MathJax<br>Preview";
1132 btn.addEventListener('click', (e) => {
1133 e.preventDefault();
1134 ta.focus();
1135 helpMarkdownPreview(ta);
1136 });
1137 ta.parentNode.appendChild(btn);
1139 function rewriteReplyHover(unit) {
1140 function getTextById(id) {
1141 let repltd = document.getElementById(id);
1142 if (repltd) {
1143 let txt = repltd.innerText,
1144 authtd = repltd.parentNode.getElementsByTagName("td")[0],
1145 author = authtd.querySelector("a.author").innerText,
1146 digest = txt.split("\n").splice(0, hoverTextLines).join("\n");
1147 return escapeChars("[[ "+author+" ]]\n"+digest);
1148 } else
1149 return "";
1151 unit = unit||document;
1152 for (let td of unit.querySelectorAll("td.repl")) {
1153 let firstC = td.firstChild;
1154 // Direct replacing innerHTML breaks embedded DOM event handlers.
1155 // So, we split td.repl into elements and replace the first
1156 // textNode(nodeType==3) with hover-text embeded content.
1157 if (firstC.nodeType==3 && firstC.nodeValue.startsWith(">#")) {
1158 let newline = firstC.nodeValue.indexOf("\n");
1159 let firstline;
1160 if (newline > 0) {
1161 firstline = firstC.nodeValue.substring(0, 1+newline);
1162 firstC.nodeValue = firstC.nodeValue.substring(1+newline);
1163 } else {
1164 // Cannot be reached here, but leave this for robustness
1165 firstline = firstC.nodeValue;
1166 firstC.nodeValue = "";
1168 td.insertAdjacentHTML(
1169 'afterbegin',
1170 escapeChars(firstline).replace(
1171 /#([0-9]+)/g,
1172 (match, start, whole) => {
1173 let id = RegExp.$1
1174 return '<a title="' + getTextById(id)
1175 + '" href="' + match
1176 + '">' + match + '</a>';
1177 }));
1181 function initReplyHover(unit) {
1182 // https://stackoverflow.com/questions/60154233/event-when-typesetting-is-done-mathjax-3
1183 if (mathjax && MathJax.startup)
1184 MathJax.startup.promise.then(()=>rewriteReplyHover());
1185 else
1186 rewriteReplyHover();
1188 function init() {
1189 isOlderJS = !("insertAdjacentElement" in document.body);
1190 initGrpAction();
1191 initBlogs();
1192 initFileInput();
1193 initTextarea();
1194 initGrphome();
1195 initMath();
1196 initReplyHover();
1198 document.addEventListener('DOMContentLoaded', init, null);
1199 })();