Mercurial > hgrepos > hgweb.cgi > s4
view s4-main.js @ 1036:9c392ddb4d8a draft
Add shortcut to sort-button
author | HIROSE Yuuji <yuuji@gentei.org> |
---|---|
date | Wed, 06 Mar 2024 09:49:08 +0900 |
parents | e8f73df7ed5d |
children | 634fee6a6bd2 |
line wrap: on
line source
// 愛 (function (){ var isOlderJS; // Set in init(); var hoverTextLines = 10; var hasTouchPad = (navigator.maxTouchPoints && navigator.maxTouchPoints >0); var myurl = document.URL, mypath = myurl.substring(myurl.lastIndexOf("/")); var art_m_list = []; var mathjax = false; let input_pdfsw = 'input[name="comppdf"]'; if (mypath.match(/(.*)\/(.*)/)) { mypath = RegExp.$2; mypath = mypath.substring(0, mypath.lastIndexOf("?")); //alert("mypath="+mypath); } function escapeChars(old) { return old.replaceAll('&', '&') .replaceAll('"', '"') .replaceAll("<", '<') .replaceAll(">", '>'); } function collectElementsByAttr(elm, attr, val) { var e = document.getElementsByTagName(elm); if (!e) return null; var list = []; for (var i of e) { if (i.getAttribute(attr) == val) list.push(i) } return list; } function nthChildOf(parent, n, elem) { // Return Nth child of type ELEM // N begins with 1 var i=0; var le = elem.toLowerCase(); for (var c of parent.childNodes) { if (!c.tagName) continue; if (c.tagName.toLowerCase() == le) { if (++i >= n) return c; } } return null; } function insertRedirect(e) { var articleId, textarea = document.getElementById("text"); var p = e.target, checked = p.checked; while (p = p.parentNode) if (p.nodeName.match(/^td$/i)) break; if (!p) return; while (p = p.nextSibling) if (p.nodeName.match(/^td$/i)) break; if (!p) return; articleId = p.getAttribute("id"); if (textarea && articleId) { var tv = textarea.value, lines; if (tv) lines = tv.split("\n"); else lines = [""]; var re = new RegExp("[, ]*#"+articleId+"(?![0-9])"); checked = (p.nodeName.match(/^input$/) ? p.checked // checkbox obeys its status : !lines[0].match(re)) // a-elment toggles redirection if (checked) { if (!lines[0].match(re)) { var re2 = new RegExp(/>#[#0-9, ]+[0-9]/); if (lines[0].match(re2)) lines[0] = lines[0].replace( re2, '$&, '+'#'+articleId); else { if (lines[0] > "") lines[0] = " "+lines[0]; lines[0] = ">#"+articleId+lines[0]; } } } else { // Remove #xxxxx if (lines[0].match(/^>#[0-9 ,]+#/)) // 2 or more #id's lines[0] = lines[0].replace( new RegExp("^>#"+articleId+"[ ,]*"), ">").replace( new RegExp("[ ,]*#"+articleId), ""); else { lines[0] = lines[0].replace( new RegExp(">#"+articleId+"[ ,]*"), ""); } } lines[0] = lines[0].replace(/^> *$/, ''); textarea.value = lines.join("\n"); } } function registPjaxViewers(aHrefList) { let apos=art_m_list.length; for (let a of aHrefList) { let href = a.getAttribute("href"); let localvar = apos; let td = a.parentNode, tr = td.parentNode, id = td.id, text = td.textContent, author = tr.getElementsByTagName("a"); if (author) author = author[0].getAttribute("title"); if (href.match(/\?showattc\+article_m\+([0-9]+)$/)) { if (td.innerHTML.match(/読み取り不可/)) { a.removeAttribute("href"); continue; } let url = RegExp.lastMatch; // console.log("pjaxView(e, "+href+", "+apos+")"); a.addEventListener("click", function(e) { // Shoud use closure local variable: localvar pjaxView(e, href, localvar); }, false); apos++; art_m_list.push({ url: href, id: id, author: author, text: text }); } } } function registInsertDirect(aHrefList) { for (i of aHrefList) if (i.getAttribute("href").match(/^#[0-9]+$/)) if (RegExp.lastMatch == i.innerHTML) i.addEventListener("click", insertRedirect, false) } function mathjaxUpdate(arg) { try { if (MathJax && MathJax.typesetPromise) { MathJax.texReset(); // Reset Math counters MathJax.typesetPromise(arg); // MathJax v3 } } catch (err) {console.log(err);} } var ajaxSubmit; function replAddNews(newtable) { let newids = [], idlist=[]; let getArticleID = function (td) { return parseInt(td.parentNode.getElementsByTagName("td")[1].id); } for (let i of newtable.querySelectorAll("td.repl")) newids.push(i); newids = newids.sort((a,b)=> { return (getArticleID(a) - getArticleID(b)); }); for (i of newids) idlist.push(getArticleID(i)); console.log("IDList="+idlist.join()); let cnt=0, ntr; let current = collectElementsByAttr("td", "class", "repl"), ncur=0, n, icur=0, o, oid, nid, otr; current = document.querySelectorAll('td[class="repl"]'); let last=current[current.length-1], tbody = last.parentNode.parentNode; let addEventsToNewTr = function(tr) { let td = tr.getElementsByTagName("td"), td0 = td[0], td1 = td[1]; td0.classList.add("new"); registInsertDirect(td0.querySelectorAll("a[href]")); registPjaxViewers(td1.querySelectorAll("a[href]")); } // Erase all "new article" flags before merging for (let i of document.querySelectorAll("td.new")) i.classList.remove("new"); // Now reconstruct articles with merge-sort like method outer: for (; ncur<newids.length; ncur++) { n = newids[ncur]; if (!n.id) continue; nid = parseInt(n.id); if (nid<=0) continue; ntr = n.parentNode; for (; icur<current.length; icur++) { o = current[icur]; otr = o.parentNode; oid = getArticleID(o); if (!oid || oid=="") continue; if (oid >= nid) { addEventsToNewTr(ntr); tbody.insertBefore(ntr, otr); if (oid==nid) otr.remove(); cnt++; continue outer; } } // Append absolutely new articles. ntr = n.parentNode; addEventsToNewTr(ntr) tbody.appendChild(atMarkView(ntr)); ntr.classList.add("dissolving"); let localntr = ntr; setTimeout(() => { localntr.classList.remove("dissolving"); localntr.classList.add("emerging"); }, 100); rewriteReplyHover(ntr); cnt++; } mathjaxUpdate(newids); console.log("Update "+cnt+"rows"); if (cnt>0 && ntr.scrollIntoView) { let option = {behavior: "smooth"}; if (!isOlderJS) option.block = "center"; try { // Scroll to last updated row ntr.scrollIntoView(option); } catch (e1) {} } return cnt; } function warnFileSize(form) { let szmax = form.querySelector('input[name="filesize_max"]').value; if (!szmax || szmax=="") return; szmax = parseInt(szmax); if (szmax <= 0) return; // szmax = 10000 let ng = "", rcval=false, fileexists=false, pdfsw = form.querySelector(input_pdfsw), pdfmsg = "Try compressing PDF?\nPDFを圧縮してみますか?\n" + "(それでも収まらない場合もあります)"; for (let f of form.querySelectorAll('input[type="file"]')) { let thiserr = false; for (let i of f.files) { fileexists = true; let fn = i.name, sz = i.size; console.log("max="+szmax+", fn="+fn+", sz="+sz); if (sz > szmax) { if (fn.match(/\.pdf/i) && sz < szmax*3 // XXX : x3 reasonable? && (pdfsw || confirm(pdfmsg))) { if (!pdfsw) { pdfsw = document.createElement("input"); pdfsw.name = "comppdf"; pdfsw.type = "hidden"; f.parentNode.insertBefore(pdfsw, f); pdfsw.value = "yes"; } } else { thiserr = true; ng += ((ng>"" ? ", " : "")+fn) } } } thiserr ? f.classList.add("warnbg") : f.classList.remove("warnbg"); } if (ng>"") { rcval = "File-size Limit Error: "+ng+"\n"+ "Should be less than "+szmax+"bytes.\n"+ szmax+"バイト未満にしてください" alert(rcval); } if (form.text.value == "") { let w; if (fileexists) w = "Fill the text area\n" + "添付したファイルに関する説明を入れてください。"; else w = "Enter your comment!\n何か書き込んでね!"; alert(w); rcval = (rcval || w); form.text.classList.add("warnbg"); setTimeout(() => {form.text.classList.remove("warnbg");}, 2000) } return rcval; } function ajaxPost(e) { e.preventDefault(); let rowid; if (!myurl.match(/replyblog\+([0-9]+)/)) return; rowid = RegExp.$1 let myform = document.querySelector("form.replyblog"); let data = new FormData(myform), fetchtime = data.get("fetchtime"); if (!fetchtime || fetchtime=="") return; ///*XX*/fetchtime = "2020-06-14T00:00:00";data.set("fetchtime", fetchtime) ajaxSubmit = e.target; ajaxSubmit.back = ajaxSubmit.textContent; if (ajaxSubmit.id == "reload") { ajaxSubmit.textContent = "更新中" data.set("text", "") } else { if (warnFileSize(myform)) return; ajaxSubmit.textContent = "送信中"; } ajaxSubmit.blur(); ajaxSubmit.disabled = true; let act = mypath+"?blog_fetch+"+rowid+"+f:"+fetchtime; function respUpdate(tbody) { ajaxSubmit.textContent = ajaxSubmit.back; ajaxSubmit.disabled = false; let div = document.createElement("div"), form, newform; try { div.innerHTML = tbody; form = div.querySelector("form"); } catch (er) { alert("Cannot parse fetch data"); return; } let update = replAddNews(form); let dispelem = myform.querySelector("textarea").parentNode; if (div.querySelector('input[name="user"]')) { // is login form dispInfoMomentary("Login Again Please", dispelem) return; } newform = new FormData(form); if (data.get("text") > "") { // Called by submit button myform.reset(); let pdfsw = myform.querySelector(input_pdfsw); if (pdfsw) pdfsw.remove(); // myform.text.value = ''; } myform.fetchtime.value = newform.get("fetchtime"); myform.id.value = newform.get("id"); if (update && update > 0) { let s = update + " new article" + (update>1 ? "s" : "") + " posted"; dispInfoMomentary(s, dispelem); } } fetch(act, { method: "POST", body: data, credentials: "include" // For older firefox }).then((resp) => { return resp.text(); }).then((tbody) => { respUpdate(tbody); }) } function pjaxView(ev, url, mynum) { if (ev.ctrlKey||ev.shiftKey) return; ev.preventDefault(); let box = document.createElement("div") box.setAttribute("class", "pjaxview"); let p1 = document.createElement("p"), bt = document.createElement("button"), sl = document.createElement("button"), sr = document.createElement("button"), loading = document.createElement("span"), info = document.createElement("p"); info1 = document.createElement("span"); info2 = document.createElement("span"); iframe = document.createElement("iframe"); var curpos = mynum; var historyBase = history.length; function _setPjaxCurposInfo() { let len = art_m_list.length; let cur = art_m_list[curpos] info1.textContent = (1+curpos)+" of "+len+" article #"+cur.id+ (cur.author ? " by "+cur.author : "") + ":"; info2.textContent = cur.text.trim(); info2.setAttribute("class", "border textdigest"); } function _resetPjax() { // All we can do surely is to back 1 page, // because we cannot move to desirable entry of history list. history.back(); } function setSwipeAct(iframe) { // We cannot use DOMContentLoaded nor iframe.contentWindow here. // PDF.js does not construct contentWindow...? iframe.addEventListener("load", () => { loading.classList.remove("loading"); if (!hasTouchPad) return; let ifm = iframe.contentDocument; let startX, moveX, thresh = 100; ifm.addEventListener("touchstart", (e) => { e.preventDefault(); startX = e.touches[0].pageX; }, false); ifm.addEventListener("touchmove", (e) => { e.preventDefault(); moveX = e.touches[0].pageX; }, false); ifm.addEventListener("touchend", (e) => { if (startX < moveX && startX + thresh < moveX) { switchTo(e, -1); } else if (startX > moveX && startX - thresh > moveX) { switchTo(e, +1); } }, false); }, false); } function switchTo(e, direction) { e.preventDefault(); let len = art_m_list.length, cur, newpos, url; newpos = (curpos+len+direction)%len; if (curpos == newpos) return; // No need to switch to same one curpos = newpos; cur = art_m_list[curpos]; url = cur.url; // We should remove iframe once to preserve history Object // https://inthetechpit.com/2019/04/20/update-iframe-without-affecting-browser-history/ let parent = iframe.parentNode; // alert("D = "+direction); iframe.remove(); parent.appendChild(iframe); try { loading.classList.add("loading"); iframe.src = url; // iframe.contentDocument.location.replace(url); // location.replace cannot be used because PDF viewer.js // does not have iframe.contentDocument } catch (err) { alert("Cannot load "+src+" : "+err.name); } _setPjaxCurposInfo(); setSwipeAct(iframe); } function switchToByKey(e) { // alert("KEY="+e.key); switch (e.key) { case "ArrowLeft": switchTo(e, -1); break; case "ArrowRight": switchTo(e, +1); break; case "Escape": history.back(); } } // <div><p> // <button> << </button><button>Dismiss</button><button> >> </button> // </p><p><span> info1 </span> <span> info2 </span></p> // <iframe src="..."></iframe> // </div> // ==> [ << ][Dissmiss][ >> ] // ==> ## of ## article #xxx by AUTHOR sl.textContent = " << "; sr.textContent = " >> "; sl.addEventListener("click", (e) => {switchTo(e, -1);}); sr.addEventListener("click", (e) => {switchTo(e, +1);}); sl.setAttribute("title", "to="+(mynum-1)); sr.setAttribute("title", "to="+(mynum+1)); document.body.appendChild(box); bt.textContent = "Click to dismiss / もどる"+mynum; box.appendChild(p1); p1.appendChild(sl); p1.appendChild(bt); p1.appendChild(sr); { // TEST: Normal mode let only = document.createElement("button"), h = location.href; only.textContent = ".oO□"; only.setAttribute("title", "Open in Normal Window"); only.addEventListener("click", function() { location.replace(iframe.src); }); p1.appendChild(only); } p1.appendChild(loading); info.appendChild(info1); info.appendChild(info2); loading.textContent=" Loading..."; loading.classList.add("hidden"); loading.classList.add("loading"); box.appendChild(info); iframe.src = url; box.addEventListener("keydown", switchToByKey); //box.addEventListener("click", (e) => {_resetPjax();}); bt.addEventListener("click", (e) => {_resetPjax();}); // dp.addEventListener("click", (e) => {_resetPjax();}); info.addEventListener("click", (e) => {_resetPjax();}); box.appendChild(iframe); setSwipeAct(iframe); _setPjaxCurposInfo(); bt.focus(); setTimeout(() => {box.classList.add("pjaxview2");}, 10); // Finally update history stack pjaxHistoryPush(box); } function pjaxHistoryPush(box) { if (history.pushState) { let h = location.href.replace(/#.*/, '')+"#pjaxview"; history.pushState({url: h}, null, h); window.addEventListener("popstate", (e) => { if (box) { box.remove(); box = null; } }, false); } } function reverseChecks() { var names = collectElementsByAttr("input", "name", "usel"); for (let u of names) { u.checked = !u.checked; } } function renumberOL(str, start) { var stra = str.split("\n"); for (var i=1; i<stra.length; i++) { if (stra[i].match(/^[1-9][0-9]*\. /)) { let orig=stra[i]; stra[i] = (++start)+". "+RegExp.rightContext; } else if (stra[i].match(/^ /)) { continue; } else break; } return stra.join("\n"); } function submitThisForm(e) { var input = e.target, ajaxpost = document.getElementById("c"); for (var elm=input.parentNode; elm; elm = elm.parentNode) { if (ajaxpost) { ajaxpost.click(); return true; } else if (elm.nodeName.match(/form/i)) { elm.submit(); return true; } } return false; } function helpMarkdownBS(e) { var area = e.target, pos = area.selectionStart, text = area.value; if (area.selectionStart != area.selectionEnd) return; if (pos<2) return; if (text.substr(pos-1, 2)=="\n\n") return; var bol = text.lastIndexOf("\n", pos-1), eol = text.indexOf("\n", pos); if (bol<=0 || bol==eol) return; var thisline = text.substring(bol+1, eol==-1 ? text.length : eol); thisline = text.substring(bol+1, pos); if (thisline == "* ") { area.setSelectionRange(pos-2, pos); } else if (thisline.match(/^[1-9][0-9]*\. $/)) { area.setSelectionRange(pos-RegExp.lastMatch.length, pos); } } function helpMarkdownEnter(e) { if (e.keyCode == 13 && !e.shiftKey) { if (e.ctrlKey && submitThisForm(e)) { e.preventDefault(); return; } var area = e.target; var pos = area.selectionStart, text = area.value; if (pos==0) return; var last = text.lastIndexOf("\n", pos-1); var rest = text.substring(pos), rest0=rest; var line = last ? text.substring(last+1, pos) : text; var next = rest.substring(rest.indexOf("\n"))||rest; next=next.substring(1); var tail = text.substring(pos-2, pos), br = (tail==" "); var add = "", offset = 1; if (line.startsWith("* ")) { add = "* "; offset += add.length; if (br) { add = " " + "\n" + add; } } else if (line.match(/^([1-9][0-9]*)\. /)) { var ln = parseInt(RegExp.$1), nn=ln+1, len = RegExp.lastMatch.length; add = nn+". "; let toeol = text.substr(pos, text.indexOf("\n")); if (br) { if (next.startsWith(add)) { add=" ".repeat(len); nn = ln; } else { add = " ".repeat(len)+ "\n" + add; offset -= len+1; } } if (next.match(/^[1-9][0-9]*\. /)) rest = renumberOL(rest, nn); offset += add.length; } else if (line.match(/^\|( *).+\|/)) { add = "|" + RegExp.$1 + " |"; offset += add.length-2; } else { return; } e.preventDefault(); if (!document.execCommand("insertText", false, "\n"+add)) { //Firefox area.selectionEnd = area.value.length; area.setRangeText("\n"+add+rest); area.selectionEnd = null; } else { area.selectionEnd = area.value.length; area.setSelectionRange(area.selectionStart, area.value.length); document.execCommand("insertText", false, rest); area.selectionEnd = null; area.focus(); } area.selectionStart = pos+offset; return; if (document.execCommand("insertText", false, "\n"+add)) { //area.setSelectionRange(area.selectionStart, text.length); // alert("rest=["+rest+"], add=["+add+"]"); alert(text.substring(pos, area.value.length)); if (rest != rest0) { area.setSelectionRange(pos, area.value.length); return; document.execCommand("delete"); } document.execCommand("insertText", false, rest); } else { // Firefox cannot use insertText in textarea... area.value = text.substring(0, pos) + "\n" + add + rest; } //area.setSelectionRange(pos+length(add)); area.selectionStart=area.selectionEnd = (pos + offset); } } var helpParenPreview = 0; function helpMarkdownParen(e) { if (!mathjax) return; var area = e.target, pos = area.selectionStart, text = area.value; if (pos<2) return; if (text[pos-1] == "\\") { let ins="( \\)"; if (text[pos-2] == "\\") ins="( \\\\)"; area.setRangeText(ins, pos, pos); area.selectionStart = pos+2; if (helpParenPreview++ < 1) { dispInfoMomentary("Preview formula by Meta-p\n"+ "Meta-p で数式プレビュー", e.target.parentNode); } e.preventDefault(); } } function textInsert(area, string, pos1, pos2) { console.log("str="+string); area.setRangeText(string, pos1||area.selectionStart, pos2||pos1||area.selectionStart); area.selectionStart += string.length; } function beginningOfLine(area, pos) { pos = pos||area.selectionStart; let b = area.value.lastIndexOf("\n", pos); if (pos>1 && area.value.charCodeAt(pos)==10) b = area.value.lastIndexOf("\n", pos-1);; return b>=0 ? b : 0; } function isInBeginEnd(area, pos){ pos = pos||area.selectionStart; let bol = beginningOfLine(area, pos); let thisline = area.value.substr(bol); console.log("curchar="+area.value.charCodeAt(pos)); console.log("prechar="+area.value.charCodeAt(pos-1)); console.log("bol="+bol+", thisline="+thisline); let match = thisline.search(/\\(begin|end){([A-Za-z]*)/), lm, be; if (match >= 0) { lm = RegExp.lastMatch; be = RegExp.$1; return RegExp.$2 } return null; } function helpMarkdownBrace(e) { if (!mathjax) return; var area = e.target, pos = area.selectionStart, text = area.value, begin = "\\begin", end = "\\end"; if (pos < end.length) return; if (text.substr(pos-end.length).startsWith(end)) { let beg = text.lastIndexOf(begin, pos); if (beg >= 0) { let env = text.substr(beg).search(/\\begin{(.*?)}/); if (env >= 0) { textInsert(area, "{"+RegExp.$1+"}", pos); e.preventDefault(); } } } } function helpMarkdownBraceClose(e) { if (!mathjax) return; let area = e.target, pos = area.selectionStart, text = area.value, begin = "\\begin", end = "\\end"; if (text.substr(pos).startsWith("}")) { area.setRangeText("", pos, pos+1); // e.preventDefault(); } let inbegend = isInBeginEnd(area, pos); if (!inbegend) return; let nextendpos = text.substr(pos).indexOf("\\end{"); let nextcurend = text.substr(pos).indexOf("\\end{"+inbegend+"}"); if (nextcurend<0 || nextendpos!=nextcurend) { area.setRangeText("}\n\n\\end{"+inbegend+"}", pos, pos); area.selectionStart = pos+2; e.preventDefault(); } console.log(inbegend); } function helpMarkdownPreview(area) { if (!mathjax) { alert("no"+e.target) dispInfoMomentary("This board has no MathJax mode.\n"+ "この掲示板は数式モードOFFです。", e.target.parentNode); return; } let text = area.value; let preview = document.createElement("div"); let bp = document.createElement("p"); let btn = document.createElement("button"); btn.innerText = "Click or ESC to Dissmiss / クリックかESCで戻る"; bp.classList.add("c"); preview.classList.add("pjaxview"); preview.classList.add("pjaxview2"); let pre = document.createElement("p"); bp.appendChild(btn); preview.appendChild(bp); preview.appendChild(pre); pre.innerText = text; document.body.appendChild(preview); function dismiss(t) { history.back(); preview.remove(); area.focus(); } preview.addEventListener("click", dismiss, false); preview.addEventListener("keydown", dismiss, false); MathJax.typesetPromise([pre]); pjaxHistoryPush(preview); btn.focus(); } function helpMarkdownAt(e) { var area = e.target, pos = area.selectionStart; if (pos == 0) { area.value = "@all" + area.value; area.selectionStart = area.selectionEnd = 4; dispInfoMomentary("@all で全員に通知します", area.parentNode); e.preventDefault(); } } function helpMarkdown(e) { switch (e.key) { case "Backspace": helpMarkdownBS(e); break; case "Enter": helpMarkdownEnter(e); break; case "(": helpMarkdownParen(e); break; case "p": if (e.metaKey) helpMarkdownPreview(e.target); break; case "{": helpMarkdownBrace(e); break; case "}": helpMarkdownBraceClose(e); break; case "@": helpMarkdownAt(e); break; } } /* Init event listeners */ function addFileInput() { var inpfile = collectElementsByAttr("input", "name", "image"); if (!inpfile) return; var filled = true; var i, ih; for (i of inpfile) { if (! i.value) filled=false; } if (filled) { ih = i.parentNode.innerHTML; let ipph = i.parentNode.parentNode.innerHTML; //Entire td inside if (ipph) { let ip = i.parentNode; var inpf = ipph.substring(ipph.indexOf('<span class="file')), newi = "<br>"+inpf.substring(0, inpf.indexOf("/span>")+6); ip.insertAdjacentHTML("afterend", newi) ip.nextSibling.nextSibling.addEventListener('change', () => { warnFileSize(document.forms[0]); }); } } } function initFileInput() { // Multiplies "input type=file" var el, morefile = document.getElementById("morefile"); if (morefile) { for (el of collectElementsByAttr("input", "name", "image")) { el.addEventListener("change", function(ev) { if (ev.target.value > "" && ev.target.files.length == 1) morefile.style.visibility = "visible"; // No need to hide again, sure? }); } morefile.addEventListener("click", addFileInput, null); } // When renaming, select basename part for (el of collectElementsByAttr("input", "class", "mv")) { el.addEventListener("focus", function(ev) { var i = ev.target; if (i) { i.setSelectionRange(0, i.value.lastIndexOf(".")); } }); } } function initTextarea() { var te = collectElementsByAttr("textarea", "name", "text"); if (!te || !te[0]) return; te[0].addEventListener("keydown", helpMarkdown, false); } function atMarkView(elem) { // Enclose "@all" with span for (i of elem.querySelectorAll("td.repl")) { if (i.textContent.startsWith("@all")) { i.firstChild.nodeValue = i.firstChild.nodeValue.substring(4); i.insertAdjacentHTML( 'afterbegin', '<div class="atall">@all</div>' ); i.classList.add("atall"); } } return elem; } var authVisible = false; function toggleAuthorVisibility(e) { // Click to hideauth button toggles visibility of author columns e.stopPropagation(); let b = document.getElementById("hideauth"); if (authVisible) { for (let i of document.querySelectorAll("td.repatt")) { i.classList.remove("hideauthor"); } authVisible = false; } else { for (let i of document.querySelectorAll("td.repatt")) { i.classList.add("hideauthor"); } authVisible = true; } b.textContent = b.textContent.replace(/OFF|ON/, authVisible ? "ON" : "OFF"); } function downloadFile(filename, content, BOM) { let bom = new Uint8Array([0xEF, 0xBB, 0xBF]); let str = new Blob(BOM ? [bom, content] : [content], {type: "text/csv"}); var uri = URL.createObjectURL(str); let a = document.createElement("a"); a.download = (BOM ? "BOM-" : "")+filename; a.href = uri; document.body.appendChild(a); a.click(); document.body.removeChild(a); } function getTextContentCSV_1(e) { let blogtbl = document.querySelector("table.blog_replies"); let needBOM = e.ctrlKey; if (!blogtbl) return; let trw = blogtbl.querySelector("tr.warn"), a; if (trw && (a=trw.querySelector("th>a"))) { if (a.title == "Show All") { if (window.confirm(`50件以下に表示制限されています。 取得し直しますか? Cancelを押すとこのまま取得します。 Seen articles limited to 50 items. Push OK to get all articles, Cancel to get only seen articles.`)) { a.click(); return; } } } if (navigator.userAgent.match(/Windows/)) { if (!e.ctrlKey && !e.shiftKey && !window.confirm(`Excelで読ませるCSVの場合はBOMが必要です。 その場合は一度キャンセルして Ctrl キーを押しながらボタンクリックして下さい。 逆にExcel以外(GoogleスプレッドシートやLibreOfficeや他のツール)で読む場合はBOMをつけるとファイルの1行目の先頭にゴミのようなものが見える場合あるのでそのときは除去する必要があります。 今後もBOM不要の場合はShiftキーを押しながらクリックして下さい。 If you feed this CSV into Microsoft Excel, consider adding BOM sequence that can be prepended by pressing Control key with click. In this case, click CSVget with ctrl key after Cancel this dialog. If you never need BOM, press Shift key with click.`)) return; } outcsv = [] for (let row of blogtbl.querySelectorAll("tr[id]")) { let tds = row.querySelectorAll("td"), a = tds[0].querySelector("a.author"), author = a.title, name = a.innerText, time = tds[0].querySelector("span").title, id = tds[1].id, body = tds[1].textContent; //console.log(`${author},${name},${time},#${id},${body}`); outcsv.push({ "author": author, "name": name, "time": time, "id": "#"+id, "body": body}); } let line = new CSV(outcsv, {header:true}).encode(), fn = myurl.replace(/.*\?/, "").replaceAll("+", "-") .replace(/#.*/, "").replace("-n:all", ""); downloadFile(fn+".csv", line, needBOM); } function getTextContentCSV(e) { if (!document.getElementById("csvminjs")) { let csvmin = document.createElement("script"); csvmin.src="https://www.yatex.org/libcache/csv.min.js"; csvmin.id = "csvminjs"; // https://stackoverflow.com/questions/14521108/dynamically-load-js-inside-js csvmin.addEventListener("load", ()=>{ getTextContentCSV_1(e)}, 10); document.querySelector("head").appendChild(csvmin); } else { getTextContentCSV_1(e); } } function initBlogs() { // (1)Auto-complete #xxxx, (2)Prepare sort // (1)Complete #xxxx let i, check = collectElementsByAttr("input", "name", "notifyto"); let blogheadtd = document.querySelector("table.bloghead tr td"); if (check) for (i of check) { i.addEventListener("click", insertRedirect, false); } registInsertDirect(document.querySelectorAll("a[href]")); if (myurl.match(/replyblog\+[0-9]/) && document.querySelector("td.repl")) { // There's no need to provide ajax posting when // no replies written to the blog. Therefore we // assign ajax post when td.repl exists. for (i of document.querySelectorAll('input#c[value="送信"]')) { let b = document.createElement("button"); b.textContent = "送信!"; console.log("b="+b+", tc="+b.textContent); b.addEventListener("click", ajaxPost, false); // i.insertAdjacentElement('afterend', b); b.setAttribute("class", i.getAttribute("class")) b.setAttribute("title", i.getAttribute("title")) i.parentNode.replaceChild(b, i); b.id = i.id; // i.remove(); i.classList.add("aux"); i.value = "送信(予備)" b.parentNode.appendChild(i); } i = document.getElementById("reload"); if (i) i.addEventListener("click", ajaxPost, false); // Add CSV download button if (blogheadtd) { let btn = document.createElement("button"); btn.innerText = "CSVget"; btn.type = "button"; btn.accessKey = "g"; btn.title = `Shortcut: G 見えている書き込みをCSVで取得します 全件表示されていることを確認してから利用して下さい。 Excelで利用する場合は Ctrl を押しながらクリックして下さい。 Get seen TEXT content as CSV.`; btn.addEventListener("click", getTextContentCSV, false); let artlink = blogheadtd.querySelector('a[accesskey="f"]'); let spacer = document.createElement("span"); if (artlink) { spacer.innerText = "|"; artlink.insertAdjacentElement('beforebegin', btn); artlink.insertAdjacentElement('beforebegin', spacer); } else { spacer.innerText = " "; blogheadtd.appendChild(spacer); blogheadtd.appendChild(btn); } } } for (i of document.querySelectorAll('input[type="file"]')) { i.addEventListener('change', (e) => { warnFileSize(document.forms[0]); }, false) } if (i=document.getElementById("hideauth")) { i.addEventListener('click', toggleAuthorVisibility, false); } // Hack article_m links registPjaxViewers(document.querySelectorAll("a[href]")); atMarkView(document); /*****************************************************************/ //(2) Prepare sort let warn = document.querySelector("tr.warn span.warn"); if (blogheadtd && warn==null) { let umode = "byUser", amode = "byArticle"; let sbtn = document.createElement("button"); sbtn.textContent = umode; sbtn.type = 'button'; sbtn.accessKey = "o"; sbtn.title = `Shortcut: O 書き込みを著者順に並べ替えます(全件表示時のみ)。 特定のユーザの書き込みを探すときに有用です。 Sort articles by UserID or ArticleID. Handy at searching for articles by certain user.`; let spacer = document.createElement("span"); spacer.textContent = " "; let hide = document.getElementById("hideauth"); if (hide) { hide.insertAdjacentElement('afterend', sbtn); hide.insertAdjacentElement('afterend', spacer); } else { blogheadtd.appendChild(spacer); blogheadtd.appendChild(sbtn); } function compareRowsByUid(a, b) { if (a.key1 < b.key1) return -1; if (a.key1 > b.key1) return 1; if (a.key2 < b.key2) return -1; if (a.key2 > b.key2) return 1; return 0; } function compareRowsByAid(a, b) { if (a.key2 < b.key2) return -1; if (a.key2 > b.key2) return 1; return 0; } sbtn.addEventListener("click", (e)=>{ let uidsort = (sbtn.textContent.indexOf(umode) >= 0); let rows = [], elm, tbl = document.querySelector("table.blog_replies"); for (let tr of tbl.rows) { elm = {}; elm.tr = tr; elm.key1 = tr.querySelector("a.author").title; // userid tr.innerHTML.match(/<a href="(#[0-9]+)">\1</); // ArticleID elm.key2 = RegExp.$1; rows.push(elm); } rows.sort(uidsort ? compareRowsByUid : compareRowsByAid); for (let r of rows) tbl.appendChild(r.tr); sbtn.textContent = uidsort ? amode : umode; }); } } function initGrpAction() { var rev = document.getElementById("reverse"); if (!rev) return; // Is not grpAction page if (rev.tagName.match(/span/i)) { rev.textContent = " 反転 "; rev.addEventListener("click", reverseChecks, null); } var emailbtn = document.getElementById("email"); emailbtn.addEventListener("click", function(ev){ // Enlarge box and Select user's checkbox if (!ev.target.checked) return; var x = collectElementsByAttr("div", "class", "foldtabs"); if (x && x[0] && x[0].style) { x[0].style.height = "10em"; } let myuid = document.getElementById("myuid"); if (myuid) { let usel = collectElementsByAttr("input", "name", "usel"); if (usel) { for (u of usel) { if (u.value == myuid.value) u.checked = true; } } } }, null); var teamsel = document.getElementById("selteam"); if (teamsel) { var usel, p, team; // Select all members of the team teamsel.addEventListener("change", function(ev) { var teamname = teamsel.value, selected = new RegExp('(^| )'+teamname+"($|,)"); usel = collectElementsByAttr("input", "name", "usel"); if (!usel) return; for (u of usel) { p = u.parentNode; // should be label if (!p) continue; if (teamname == "TEAM") { // Reset all checks u.checked = false; // when "TEAM" is selected } else { p = p.parentNode.parentNode;// should be tr team = nthChildOf(p, 5, "td") if (team && team.textContent && team.textContent.match(selected)) { u.checked = true; } } } }, null); } } function dispInfoMomentary(msg, elem) { // Momentarily display MSG in tooltip-baloon relative to ELEM element. let help = document.createElement("p"); elem.style.position = 'relative'; elem.style.overflow = 'visible'; help.setAttribute("class", "info-tooltip"); help.innerHTML = msg; elem.appendChild(help); setTimeout(() => { help.classList.add("dissolving"); setTimeout(() => help.remove(), 3000); }, 1000); } function initGrphome() { console.log("initGrphome"); // (1)Setup Frozen State Changing Button var ja = navigator.language.match(/ja/i); function toggleFrozen(e, rowid) { let tgt = mypath+"?blog_setfrozen+"+rowid; let td = e.target.parentNode; let tr = td.parentNode; fetch(tgt, { method: "POST", headers: {'Content-Type': 'text/html; charset=utf-8'}, credentials: "include" }).then(function(resp) { return resp.text(); }).then(function(tbody) { try { var json = JSON.parse(tbody); } catch (e) { return; } let state = json.state, newstate, info; if (json.alert) { alert(json.alert) } if (state.match(/frozen/i)) { newstate = "凍結"; info = ja ? newstate+"に設定しました" : 'Set Frozen'; } else { newstate = null; info = ja ? '稼動に設定しました' : 'Set Running'; } tr.setAttribute("class", newstate); dispInfoMomentary(info, td); }); } let btn = document.querySelectorAll("button.toggle-frozen"); for (let b of btn) { let rowid = null; let td=b.parentNode, tr = td.parentNode, fr, ru; ru = ja ? "動" : "Running"; fr = ja ? "凍" : "Frozen"; b.setAttribute('frozen-marker', fr); b.setAttribute('running-marker', ru); for (let a of tr.querySelectorAll("a[href]")) { if (a.getAttribute("href").match(/\?replyblog\+([0-9]+)/)) { rowid = parseInt(RegExp.$1); break; } } if (rowid && rowid>0) { b.addEventListener("click", function(e) { if (!btn) return; toggleFrozen(e, rowid); }, false); b.setAttribute("title", "稼動/凍結をその場で切り替えます\n\ Toggle Running/Frozen ("+rowid+")"); } } // (2)Setup Column Collapse Button // INCOMPLETE: Cannot restore original state, but it's enough... function toggleColmnWidth(th) { let tbl = document.querySelector("table.dumpblogs"); let colname = th.textContent, newwidth; if (th.style.width) { newwidth = null // https://developer.mozilla.org/ja/docs/Web/CSS/table-layout tbl.style.tableLayout = 'auto'; tbl.style.width = null; } else { newwidth = "2em"; tbl.style.tableLayout = 'fixed'; tbl.style.width = '100%'; } th.style.width = newwidth; th.style.overflow = "hidden"; for (let td of document.querySelectorAll("td."+colname)) { console.log(td.tagName); td.style.width = newwidth; console.log(td.style.width); } } let row1 = document.querySelector("table.dumpblogs tr:first-child"); if (row1) { let heads = row1.querySelectorAll("th"); for (let h of heads) { h.addEventListener("click", function(e) { toggleColmnWidth(h); }, false); h.setAttribute("title", "Click to shrink these columns"); } } } function initMath() { mathjax = window.MathJax||document.getElementById("mathjax"); if (!mathjax) return; let ta = document.querySelector("textarea"); if (!ta) return; let btn = document.createElement("button"); btn.setAttribute("title", "\\( と \\) で数式利用\n"+ "\\[ と \\] で段組み数式モード\n"+ "便利なマクロ:\n"+ " \\boxed{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+ " \\underline{aaa}, \\fcolorbox{framecolor}{bgcolor}{text}\n"+ "独自定義マクロ:\n"+ " \\warn{xxx} 注意喚起用色付き枠\n"+ " \\Warn{xxx} 大きな文字で注意喚起") btn.innerHTML = "MathJax<br>Preview"; btn.addEventListener('click', (e) => { e.preventDefault(); ta.focus(); helpMarkdownPreview(ta); }); ta.parentNode.appendChild(btn); } function rewriteReplyHover(unit) { function getTextById(id) { let repltd = document.getElementById(id); if (repltd) { let txt = repltd.innerText, authtd = repltd.parentNode.getElementsByTagName("td")[0], author = authtd.querySelector("a.author").innerText, digest = txt.split("\n").splice(0, hoverTextLines).join("\n"); return escapeChars("[[ "+author+" ]]\n"+digest); } else return ""; } unit = unit||document; for (let td of unit.querySelectorAll("td.repl")) { let firstC = td.firstChild; // Direct replacing innerHTML breaks embedded DOM event handlers. // So, we split td.repl into elements and replace the first // textNode(nodeType==3) with hover-text embeded content. if (firstC.nodeType==3 && firstC.nodeValue.startsWith(">#")) { let newline = firstC.nodeValue.indexOf("\n"); let firstline; if (newline > 0) { firstline = firstC.nodeValue.substring(0, 1+newline); firstC.nodeValue = firstC.nodeValue.substring(1+newline); } else { // Cannot be reached here, but leave this for robustness firstline = firstC.nodeValue; firstC.nodeValue = ""; } td.insertAdjacentHTML( 'afterbegin', escapeChars(firstline).replace( /#([0-9]+)/g, (match, start, whole) => { let id = RegExp.$1 return '<a title="' + getTextById(id) + '" href="' + match + '">' + match + '</a>'; })); } } } function initReplyHover(unit) { // https://stackoverflow.com/questions/60154233/event-when-typesetting-is-done-mathjax-3 if (mathjax && MathJax.startup) MathJax.startup.promise.then(()=>rewriteReplyHover()); else rewriteReplyHover(); } var getHandoutCSV; function initGetHandoutCSV() { getHandoutCSV = document.getElementById("gethandoutcsv"); if (!getHandoutCSV) return; document.getElementById("bommsg").innerText = `Excelで開く場合は上記CSVリンクをCtrlを押しながら。 You need to click CSV link above with Ctrl-key when you handle CSV with Excel`; getHandoutCSV.addEventListener("click", (e) => { e.preventDefault(); // Stop visiting link let bom = e.ctrlKey; let csv = document.getElementById("totalcsv"); if (!csv || !csv.textContent) return; let fn = "report-count", plus=myurl.lastIndexOf("+"); if (plus) fn += ("-"+myurl.substring(1+plus)); fn = fn.replace(/#.*/, ""); downloadFile(fn+".csv", csv.textContent, bom); }); } function init() { isOlderJS = !("insertAdjacentElement" in document.body); initGrpAction(); initBlogs(); initFileInput(); initTextarea(); initGrphome(); initMath(); initReplyHover(); initGetHandoutCSV(); } document.addEventListener('DOMContentLoaded', init, null); })();