s4

view s4-main.js @ 1037:634fee6a6bd2

Add row number to title attribute after sort.
author HIROSE Yuuji <yuuji@gentei.org>
date Wed, 06 Mar 2024 09:55:20 +0900
parents 9c392ddb4d8a
children 78e904f9be34
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 // (1)Auto-complete #xxxx, (2)Prepare sort
899 // (1)Complete #xxxx
900 let i, check = collectElementsByAttr("input", "name", "notifyto");
901 let blogheadtd = document.querySelector("table.bloghead tr td");
902 if (check)
903 for (i of check) {
904 i.addEventListener("click", insertRedirect, false);
905 }
906 registInsertDirect(document.querySelectorAll("a[href]"));
907 if (myurl.match(/replyblog\+[0-9]/)
908 && document.querySelector("td.repl")) {
909 // There's no need to provide ajax posting when
910 // no replies written to the blog. Therefore we
911 // assign ajax post when td.repl exists.
912 for (i of document.querySelectorAll('input#c[value="送信"]')) {
913 let b = document.createElement("button");
914 b.textContent = "送信!";
915 console.log("b="+b+", tc="+b.textContent);
916 b.addEventListener("click", ajaxPost, false);
917 // i.insertAdjacentElement('afterend', b);
918 b.setAttribute("class", i.getAttribute("class"))
919 b.setAttribute("title", i.getAttribute("title"))
920 i.parentNode.replaceChild(b, i);
921 b.id = i.id;
922 // i.remove();
923 i.classList.add("aux");
924 i.value = "送信(予備)"
925 b.parentNode.appendChild(i);
926 }
927 i = document.getElementById("reload");
928 if (i) i.addEventListener("click", ajaxPost, false);
929 // Add CSV download button
930 if (blogheadtd) {
931 let btn = document.createElement("button");
932 btn.innerText = "CSVget";
933 btn.type = "button";
934 btn.accessKey = "g";
935 btn.title = `Shortcut: G
936 見えている書き込みをCSVで取得します
937 全件表示されていることを確認してから利用して下さい。
938 Excelで利用する場合は Ctrl を押しながらクリックして下さい。
939 Get seen TEXT content as CSV.`;
940 btn.addEventListener("click", getTextContentCSV, false);
941 let artlink = blogheadtd.querySelector('a[accesskey="f"]');
942 let spacer = document.createElement("span");
943 if (artlink) {
944 spacer.innerText = "|";
945 artlink.insertAdjacentElement('beforebegin', btn);
946 artlink.insertAdjacentElement('beforebegin', spacer);
947 } else {
948 spacer.innerText = " ";
949 blogheadtd.appendChild(spacer);
950 blogheadtd.appendChild(btn);
951 }
952 }
953 }
954 for (i of document.querySelectorAll('input[type="file"]')) {
955 i.addEventListener('change', (e) => {
956 warnFileSize(document.forms[0]);
957 }, false)
958 }
959 if (i=document.getElementById("hideauth")) {
960 i.addEventListener('click', toggleAuthorVisibility, false);
961 }
962 // Hack article_m links
963 registPjaxViewers(document.querySelectorAll("a[href]"));
964 atMarkView(document);
965 /*****************************************************************/
966 //(2) Prepare sort
967 let warn = document.querySelector("tr.warn span.warn");
968 if (blogheadtd && warn==null) {
969 let umode = "byUser", amode = "byArticle";
970 let sbtn = document.createElement("button");
971 sbtn.textContent = umode;
972 sbtn.type = 'button';
973 sbtn.accessKey = "o";
974 sbtn.title = `Shortcut: O
975 書き込みを著者順に並べ替えます(全件表示時のみ)。
976 特定のユーザの書き込みを探すときに有用です。
977 Sort articles by UserID or ArticleID.
978 Handy at searching for articles by certain user.`;
979 let spacer = document.createElement("span");
980 spacer.textContent = " ";
981 let hide = document.getElementById("hideauth");
982 if (hide) {
983 hide.insertAdjacentElement('afterend', sbtn);
984 hide.insertAdjacentElement('afterend', spacer);
985 } else {
986 blogheadtd.appendChild(spacer);
987 blogheadtd.appendChild(sbtn);
988 }
989 function compareRowsByUid(a, b) {
990 if (a.key1 < b.key1) return -1;
991 if (a.key1 > b.key1) return 1;
992 if (a.key2 < b.key2) return -1;
993 if (a.key2 > b.key2) return 1;
994 return 0;
995 }
996 function compareRowsByAid(a, b) {
997 if (a.key2 < b.key2) return -1;
998 if (a.key2 > b.key2) return 1;
999 return 0;
1001 sbtn.addEventListener("click", (e)=>{
1002 let uidsort = (sbtn.textContent.indexOf(umode) >= 0);
1003 let rows = [], elm, i=0,
1004 tbl = document.querySelector("table.blog_replies");
1005 for (let tr of tbl.rows) {
1006 elm = {};
1007 elm.tr = tr;
1008 elm.key1 = tr.querySelector("a.author").title; // userid
1009 tr.innerHTML.match(/<a href="(#[0-9]+)">\1</); // ArticleID
1010 elm.key2 = RegExp.$1;
1011 rows.push(elm);
1013 rows.sort(uidsort ? compareRowsByUid : compareRowsByAid);
1014 for (let r of rows) {
1015 tbl.appendChild(r.tr);
1016 r.tr.title = ++i;
1018 sbtn.textContent = uidsort ? amode : umode;
1019 });
1022 function initGrpAction() {
1023 var rev = document.getElementById("reverse");
1024 if (!rev) return; // Is not grpAction page
1025 if (rev.tagName.match(/span/i)) {
1026 rev.textContent = " 反転 ";
1027 rev.addEventListener("click", reverseChecks, null);
1029 var emailbtn = document.getElementById("email");
1030 emailbtn.addEventListener("click", function(ev){
1031 // Enlarge box and Select user's checkbox
1032 if (!ev.target.checked) return;
1033 var x = collectElementsByAttr("div", "class", "foldtabs");
1034 if (x && x[0] && x[0].style) {
1035 x[0].style.height = "10em";
1037 let myuid = document.getElementById("myuid");
1038 if (myuid) {
1039 let usel = collectElementsByAttr("input", "name", "usel");
1040 if (usel) {
1041 for (u of usel) {
1042 if (u.value == myuid.value)
1043 u.checked = true;
1047 }, null);
1048 var teamsel = document.getElementById("selteam");
1049 if (teamsel) {
1050 var usel, p, team;
1051 // Select all members of the team
1052 teamsel.addEventListener("change", function(ev) {
1053 var teamname = teamsel.value,
1054 selected = new RegExp('(^| )'+teamname+"($|,)");
1055 usel = collectElementsByAttr("input", "name", "usel");
1056 if (!usel) return;
1057 for (u of usel) {
1058 p = u.parentNode; // should be label
1059 if (!p) continue;
1060 if (teamname == "TEAM") { // Reset all checks
1061 u.checked = false; // when "TEAM" is selected
1062 } else {
1063 p = p.parentNode.parentNode;// should be tr
1064 team = nthChildOf(p, 5, "td")
1065 if (team && team.textContent
1066 && team.textContent.match(selected)) {
1067 u.checked = true;
1071 }, null);
1074 function dispInfoMomentary(msg, elem) {
1075 // Momentarily display MSG in tooltip-baloon relative to ELEM element.
1076 let help = document.createElement("p");
1077 elem.style.position = 'relative';
1078 elem.style.overflow = 'visible';
1079 help.setAttribute("class", "info-tooltip");
1080 help.innerHTML = msg;
1081 elem.appendChild(help);
1082 setTimeout(() => {
1083 help.classList.add("dissolving");
1084 setTimeout(() => help.remove(), 3000);
1085 }, 1000);
1087 function initGrphome() {
1088 console.log("initGrphome");
1089 // (1)Setup Frozen State Changing Button
1090 var ja = navigator.language.match(/ja/i);
1092 function toggleFrozen(e, rowid) {
1093 let tgt = mypath+"?blog_setfrozen+"+rowid;
1094 let td = e.target.parentNode;
1095 let tr = td.parentNode;
1096 fetch(tgt, {
1097 method: "POST",
1098 headers: {'Content-Type': 'text/html; charset=utf-8'},
1099 credentials: "include"
1100 }).then(function(resp) {
1101 return resp.text();
1102 }).then(function(tbody) {
1103 try {
1104 var json = JSON.parse(tbody);
1105 } catch (e) {
1106 return;
1108 let state = json.state, newstate, info;
1109 if (json.alert) {
1110 alert(json.alert)
1112 if (state.match(/frozen/i)) {
1113 newstate = "凍結";
1114 info = ja ? newstate+"に設定しました" : 'Set Frozen';
1115 } else {
1116 newstate = null;
1117 info = ja ? '稼動に設定しました' : 'Set Running';
1119 tr.setAttribute("class", newstate);
1120 dispInfoMomentary(info, td);
1121 });
1123 let btn = document.querySelectorAll("button.toggle-frozen");
1124 for (let b of btn) {
1125 let rowid = null;
1126 let td=b.parentNode, tr = td.parentNode, fr, ru;
1127 ru = ja ? "動" : "Running";
1128 fr = ja ? "凍" : "Frozen";
1129 b.setAttribute('frozen-marker', fr);
1130 b.setAttribute('running-marker', ru);
1131 for (let a of tr.querySelectorAll("a[href]")) {
1132 if (a.getAttribute("href").match(/\?replyblog\+([0-9]+)/)) {
1133 rowid = parseInt(RegExp.$1);
1134 break;
1137 if (rowid && rowid>0) {
1138 b.addEventListener("click", function(e) {
1139 if (!btn) return;
1140 toggleFrozen(e, rowid);
1141 }, false);
1142 b.setAttribute("title", "稼動/凍結をその場で切り替えます\n\
1143 Toggle Running/Frozen ("+rowid+")");
1146 // (2)Setup Column Collapse Button
1147 // INCOMPLETE: Cannot restore original state, but it's enough...
1148 function toggleColmnWidth(th) {
1149 let tbl = document.querySelector("table.dumpblogs");
1150 let colname = th.textContent, newwidth;
1151 if (th.style.width) {
1152 newwidth = null
1153 // https://developer.mozilla.org/ja/docs/Web/CSS/table-layout
1154 tbl.style.tableLayout = 'auto';
1155 tbl.style.width = null;
1156 } else {
1157 newwidth = "2em";
1158 tbl.style.tableLayout = 'fixed';
1159 tbl.style.width = '100%';
1161 th.style.width = newwidth;
1162 th.style.overflow = "hidden";
1163 for (let td of document.querySelectorAll("td."+colname)) {
1164 console.log(td.tagName);
1165 td.style.width = newwidth;
1166 console.log(td.style.width);
1169 let row1 = document.querySelector("table.dumpblogs tr:first-child");
1170 if (row1) {
1171 let heads = row1.querySelectorAll("th");
1172 for (let h of heads) {
1173 h.addEventListener("click", function(e) {
1174 toggleColmnWidth(h);
1175 }, false);
1176 h.setAttribute("title", "Click to shrink these columns");
1180 function initMath() {
1181 mathjax = window.MathJax||document.getElementById("mathjax");
1182 if (!mathjax) return;
1183 let ta = document.querySelector("textarea");
1184 if (!ta) return;
1185 let btn = document.createElement("button");
1186 btn.setAttribute("title", "\\( と \\) で数式利用\n"+
1187 "\\[ と \\] で段組み数式モード\n"+
1188 "便利なマクロ:\n"+
1189 " \\boxed{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1190 " \\underline{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1191 "独自定義マクロ:\n"+
1192 " \\warn{xxx} 注意喚起用色付き枠\n"+
1193 " \\Warn{xxx} 大きな文字で注意喚起")
1194 btn.innerHTML = "MathJax<br>Preview";
1195 btn.addEventListener('click', (e) => {
1196 e.preventDefault();
1197 ta.focus();
1198 helpMarkdownPreview(ta);
1199 });
1200 ta.parentNode.appendChild(btn);
1202 function rewriteReplyHover(unit) {
1203 function getTextById(id) {
1204 let repltd = document.getElementById(id);
1205 if (repltd) {
1206 let txt = repltd.innerText,
1207 authtd = repltd.parentNode.getElementsByTagName("td")[0],
1208 author = authtd.querySelector("a.author").innerText,
1209 digest = txt.split("\n").splice(0, hoverTextLines).join("\n");
1210 return escapeChars("[[ "+author+" ]]\n"+digest);
1211 } else
1212 return "";
1214 unit = unit||document;
1215 for (let td of unit.querySelectorAll("td.repl")) {
1216 let firstC = td.firstChild;
1217 // Direct replacing innerHTML breaks embedded DOM event handlers.
1218 // So, we split td.repl into elements and replace the first
1219 // textNode(nodeType==3) with hover-text embeded content.
1220 if (firstC.nodeType==3 && firstC.nodeValue.startsWith(">#")) {
1221 let newline = firstC.nodeValue.indexOf("\n");
1222 let firstline;
1223 if (newline > 0) {
1224 firstline = firstC.nodeValue.substring(0, 1+newline);
1225 firstC.nodeValue = firstC.nodeValue.substring(1+newline);
1226 } else {
1227 // Cannot be reached here, but leave this for robustness
1228 firstline = firstC.nodeValue;
1229 firstC.nodeValue = "";
1231 td.insertAdjacentHTML(
1232 'afterbegin',
1233 escapeChars(firstline).replace(
1234 /#([0-9]+)/g,
1235 (match, start, whole) => {
1236 let id = RegExp.$1
1237 return '<a title="' + getTextById(id)
1238 + '" href="' + match
1239 + '">' + match + '</a>';
1240 }));
1244 function initReplyHover(unit) {
1245 // https://stackoverflow.com/questions/60154233/event-when-typesetting-is-done-mathjax-3
1246 if (mathjax && MathJax.startup)
1247 MathJax.startup.promise.then(()=>rewriteReplyHover());
1248 else
1249 rewriteReplyHover();
1251 var getHandoutCSV;
1252 function initGetHandoutCSV() {
1253 getHandoutCSV = document.getElementById("gethandoutcsv");
1254 if (!getHandoutCSV) return;
1255 document.getElementById("bommsg").innerText =
1256 `Excelで開く場合は上記CSVリンクをCtrlを押しながら。
1257 You need to click CSV link above with Ctrl-key when you handle CSV with Excel`;
1258 getHandoutCSV.addEventListener("click", (e) => {
1259 e.preventDefault(); // Stop visiting link
1260 let bom = e.ctrlKey;
1261 let csv = document.getElementById("totalcsv");
1262 if (!csv || !csv.textContent) return;
1263 let fn = "report-count", plus=myurl.lastIndexOf("+");
1264 if (plus) fn += ("-"+myurl.substring(1+plus));
1265 fn = fn.replace(/#.*/, "");
1266 downloadFile(fn+".csv", csv.textContent, bom);
1267 });
1269 function init() {
1270 isOlderJS = !("insertAdjacentElement" in document.body);
1271 initGrpAction();
1272 initBlogs();
1273 initFileInput();
1274 initTextarea();
1275 initGrphome();
1276 initMath();
1277 initReplyHover();
1278 initGetHandoutCSV();
1280 document.addEventListener('DOMContentLoaded', init, null);
1281 })();