s4

view s4-main.js @ 997:f7cd4528926b

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