s4

view s4-main.js @ 1035:e8f73df7ed5d

Add "sort by uid" button to blog table
author HIROSE Yuuji <yuuji@gentei.org>
date Wed, 06 Mar 2024 09:37:52 +0900
parents 1ffaa8b2b1bf
children 9c392ddb4d8a
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 = "UserSORT", amode = "ArticleSort";
970 let sbtn = document.createElement("button");
971 sbtn.textContent = umode;
972 sbtn.type = 'button';
973 let spacer = document.createElement("span");
974 spacer.textContent = " ";
975 let hide = document.getElementById("hideauth");
976 if (hide) {
977 hide.insertAdjacentElement('afterend', sbtn);
978 hide.insertAdjacentElement('afterend', spacer);
979 } else {
980 blogheadtd.appendChild(spacer);
981 blogheadtd.appendChild(sbtn);
982 }
983 function compareRowsByUid(a, b) {
984 if (a.key1 < b.key1) return -1;
985 if (a.key1 > b.key1) return 1;
986 if (a.key2 < b.key2) return -1;
987 if (a.key2 > b.key2) return 1;
988 return 0;
989 }
990 function compareRowsByAid(a, b) {
991 if (a.key2 < b.key2) return -1;
992 if (a.key2 > b.key2) return 1;
993 return 0;
994 }
995 sbtn.addEventListener("click", (e)=>{
996 let uidsort = (sbtn.textContent.indexOf(umode) >= 0);
997 let rows = [], elm,
998 tbl = document.querySelector("table.blog_replies");
999 for (let tr of tbl.rows) {
1000 elm = {};
1001 elm.tr = tr;
1002 elm.key1 = tr.querySelector("a.author").title; // userid
1003 tr.innerHTML.match(/<a href="(#[0-9]+)">\1</); // ArticleID
1004 elm.key2 = RegExp.$1;
1005 rows.push(elm);
1007 rows.sort(uidsort ? compareRowsByUid : compareRowsByAid);
1008 for (let r of rows) tbl.appendChild(r.tr);
1009 sbtn.textContent = uidsort ? amode : umode;
1010 });
1013 function initGrpAction() {
1014 var rev = document.getElementById("reverse");
1015 if (!rev) return; // Is not grpAction page
1016 if (rev.tagName.match(/span/i)) {
1017 rev.textContent = " 反転 ";
1018 rev.addEventListener("click", reverseChecks, null);
1020 var emailbtn = document.getElementById("email");
1021 emailbtn.addEventListener("click", function(ev){
1022 // Enlarge box and Select user's checkbox
1023 if (!ev.target.checked) return;
1024 var x = collectElementsByAttr("div", "class", "foldtabs");
1025 if (x && x[0] && x[0].style) {
1026 x[0].style.height = "10em";
1028 let myuid = document.getElementById("myuid");
1029 if (myuid) {
1030 let usel = collectElementsByAttr("input", "name", "usel");
1031 if (usel) {
1032 for (u of usel) {
1033 if (u.value == myuid.value)
1034 u.checked = true;
1038 }, null);
1039 var teamsel = document.getElementById("selteam");
1040 if (teamsel) {
1041 var usel, p, team;
1042 // Select all members of the team
1043 teamsel.addEventListener("change", function(ev) {
1044 var teamname = teamsel.value,
1045 selected = new RegExp('(^| )'+teamname+"($|,)");
1046 usel = collectElementsByAttr("input", "name", "usel");
1047 if (!usel) return;
1048 for (u of usel) {
1049 p = u.parentNode; // should be label
1050 if (!p) continue;
1051 if (teamname == "TEAM") { // Reset all checks
1052 u.checked = false; // when "TEAM" is selected
1053 } else {
1054 p = p.parentNode.parentNode;// should be tr
1055 team = nthChildOf(p, 5, "td")
1056 if (team && team.textContent
1057 && team.textContent.match(selected)) {
1058 u.checked = true;
1062 }, null);
1065 function dispInfoMomentary(msg, elem) {
1066 // Momentarily display MSG in tooltip-baloon relative to ELEM element.
1067 let help = document.createElement("p");
1068 elem.style.position = 'relative';
1069 elem.style.overflow = 'visible';
1070 help.setAttribute("class", "info-tooltip");
1071 help.innerHTML = msg;
1072 elem.appendChild(help);
1073 setTimeout(() => {
1074 help.classList.add("dissolving");
1075 setTimeout(() => help.remove(), 3000);
1076 }, 1000);
1078 function initGrphome() {
1079 console.log("initGrphome");
1080 // (1)Setup Frozen State Changing Button
1081 var ja = navigator.language.match(/ja/i);
1083 function toggleFrozen(e, rowid) {
1084 let tgt = mypath+"?blog_setfrozen+"+rowid;
1085 let td = e.target.parentNode;
1086 let tr = td.parentNode;
1087 fetch(tgt, {
1088 method: "POST",
1089 headers: {'Content-Type': 'text/html; charset=utf-8'},
1090 credentials: "include"
1091 }).then(function(resp) {
1092 return resp.text();
1093 }).then(function(tbody) {
1094 try {
1095 var json = JSON.parse(tbody);
1096 } catch (e) {
1097 return;
1099 let state = json.state, newstate, info;
1100 if (json.alert) {
1101 alert(json.alert)
1103 if (state.match(/frozen/i)) {
1104 newstate = "凍結";
1105 info = ja ? newstate+"に設定しました" : 'Set Frozen';
1106 } else {
1107 newstate = null;
1108 info = ja ? '稼動に設定しました' : 'Set Running';
1110 tr.setAttribute("class", newstate);
1111 dispInfoMomentary(info, td);
1112 });
1114 let btn = document.querySelectorAll("button.toggle-frozen");
1115 for (let b of btn) {
1116 let rowid = null;
1117 let td=b.parentNode, tr = td.parentNode, fr, ru;
1118 ru = ja ? "動" : "Running";
1119 fr = ja ? "凍" : "Frozen";
1120 b.setAttribute('frozen-marker', fr);
1121 b.setAttribute('running-marker', ru);
1122 for (let a of tr.querySelectorAll("a[href]")) {
1123 if (a.getAttribute("href").match(/\?replyblog\+([0-9]+)/)) {
1124 rowid = parseInt(RegExp.$1);
1125 break;
1128 if (rowid && rowid>0) {
1129 b.addEventListener("click", function(e) {
1130 if (!btn) return;
1131 toggleFrozen(e, rowid);
1132 }, false);
1133 b.setAttribute("title", "稼動/凍結をその場で切り替えます\n\
1134 Toggle Running/Frozen ("+rowid+")");
1137 // (2)Setup Column Collapse Button
1138 // INCOMPLETE: Cannot restore original state, but it's enough...
1139 function toggleColmnWidth(th) {
1140 let tbl = document.querySelector("table.dumpblogs");
1141 let colname = th.textContent, newwidth;
1142 if (th.style.width) {
1143 newwidth = null
1144 // https://developer.mozilla.org/ja/docs/Web/CSS/table-layout
1145 tbl.style.tableLayout = 'auto';
1146 tbl.style.width = null;
1147 } else {
1148 newwidth = "2em";
1149 tbl.style.tableLayout = 'fixed';
1150 tbl.style.width = '100%';
1152 th.style.width = newwidth;
1153 th.style.overflow = "hidden";
1154 for (let td of document.querySelectorAll("td."+colname)) {
1155 console.log(td.tagName);
1156 td.style.width = newwidth;
1157 console.log(td.style.width);
1160 let row1 = document.querySelector("table.dumpblogs tr:first-child");
1161 if (row1) {
1162 let heads = row1.querySelectorAll("th");
1163 for (let h of heads) {
1164 h.addEventListener("click", function(e) {
1165 toggleColmnWidth(h);
1166 }, false);
1167 h.setAttribute("title", "Click to shrink these columns");
1171 function initMath() {
1172 mathjax = window.MathJax||document.getElementById("mathjax");
1173 if (!mathjax) return;
1174 let ta = document.querySelector("textarea");
1175 if (!ta) return;
1176 let btn = document.createElement("button");
1177 btn.setAttribute("title", "\\( と \\) で数式利用\n"+
1178 "\\[ と \\] で段組み数式モード\n"+
1179 "便利なマクロ:\n"+
1180 " \\boxed{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1181 " \\underline{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+
1182 "独自定義マクロ:\n"+
1183 " \\warn{xxx} 注意喚起用色付き枠\n"+
1184 " \\Warn{xxx} 大きな文字で注意喚起")
1185 btn.innerHTML = "MathJax<br>Preview";
1186 btn.addEventListener('click', (e) => {
1187 e.preventDefault();
1188 ta.focus();
1189 helpMarkdownPreview(ta);
1190 });
1191 ta.parentNode.appendChild(btn);
1193 function rewriteReplyHover(unit) {
1194 function getTextById(id) {
1195 let repltd = document.getElementById(id);
1196 if (repltd) {
1197 let txt = repltd.innerText,
1198 authtd = repltd.parentNode.getElementsByTagName("td")[0],
1199 author = authtd.querySelector("a.author").innerText,
1200 digest = txt.split("\n").splice(0, hoverTextLines).join("\n");
1201 return escapeChars("[[ "+author+" ]]\n"+digest);
1202 } else
1203 return "";
1205 unit = unit||document;
1206 for (let td of unit.querySelectorAll("td.repl")) {
1207 let firstC = td.firstChild;
1208 // Direct replacing innerHTML breaks embedded DOM event handlers.
1209 // So, we split td.repl into elements and replace the first
1210 // textNode(nodeType==3) with hover-text embeded content.
1211 if (firstC.nodeType==3 && firstC.nodeValue.startsWith(">#")) {
1212 let newline = firstC.nodeValue.indexOf("\n");
1213 let firstline;
1214 if (newline > 0) {
1215 firstline = firstC.nodeValue.substring(0, 1+newline);
1216 firstC.nodeValue = firstC.nodeValue.substring(1+newline);
1217 } else {
1218 // Cannot be reached here, but leave this for robustness
1219 firstline = firstC.nodeValue;
1220 firstC.nodeValue = "";
1222 td.insertAdjacentHTML(
1223 'afterbegin',
1224 escapeChars(firstline).replace(
1225 /#([0-9]+)/g,
1226 (match, start, whole) => {
1227 let id = RegExp.$1
1228 return '<a title="' + getTextById(id)
1229 + '" href="' + match
1230 + '">' + match + '</a>';
1231 }));
1235 function initReplyHover(unit) {
1236 // https://stackoverflow.com/questions/60154233/event-when-typesetting-is-done-mathjax-3
1237 if (mathjax && MathJax.startup)
1238 MathJax.startup.promise.then(()=>rewriteReplyHover());
1239 else
1240 rewriteReplyHover();
1242 var getHandoutCSV;
1243 function initGetHandoutCSV() {
1244 getHandoutCSV = document.getElementById("gethandoutcsv");
1245 if (!getHandoutCSV) return;
1246 document.getElementById("bommsg").innerText =
1247 `Excelで開く場合は上記CSVリンクをCtrlを押しながら。
1248 You need to click CSV link above with Ctrl-key when you handle CSV with Excel`;
1249 getHandoutCSV.addEventListener("click", (e) => {
1250 e.preventDefault(); // Stop visiting link
1251 let bom = e.ctrlKey;
1252 let csv = document.getElementById("totalcsv");
1253 if (!csv || !csv.textContent) return;
1254 let fn = "report-count", plus=myurl.lastIndexOf("+");
1255 if (plus) fn += ("-"+myurl.substring(1+plus));
1256 fn = fn.replace(/#.*/, "");
1257 downloadFile(fn+".csv", csv.textContent, bom);
1258 });
1260 function init() {
1261 isOlderJS = !("insertAdjacentElement" in document.body);
1262 initGrpAction();
1263 initBlogs();
1264 initFileInput();
1265 initTextarea();
1266 initGrphome();
1267 initMath();
1268 initReplyHover();
1269 initGetHandoutCSV();
1271 document.addEventListener('DOMContentLoaded', init, null);
1272 })();