s4

view s4-main.js @ 1000:ddf85e80f64e

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