2 JavaScript utilities for PmWiki
3 (c) 2009-2022 Petko Yotov www.pmwiki.org/petko
4 based on PmWiki addons DeObMail, AutoTOC and Ape
5 licensed GNU GPLv2 or any more recent version released by the FSF.
7 libsortable() "Sortable tables" adapted for PmWiki from
8 a Public Domain event listener by github.com/tofsjonas
12 function aE(el, ev, fn) {
13 if(typeof el == 'string') el = dqsa(el);
14 for(var i=0; i<el.length; i++) el[i].addEventListener(ev, fn);
16 function dqs(str) { return document.querySelector(str); }
17 function dqsa(str) { return document.querySelectorAll(str); }
18 function tap(q, fn) { aE(q, 'click', fn); };
19 function pf(x) {var y = parseFloat(x); return isNaN(y)? 0:y; }
20 function zpad(n) {return (n<10)?"0"+n : n; }
21 function adjbb(el, html) { el.insertAdjacentHTML('beforebegin', html); }
22 function adjbe(el, html) { el.insertAdjacentHTML('beforeend', html); }
23 function adjab(el, html) { el.insertAdjacentHTML('afterbegin', html); }
24 function adjae(el, html) { el.insertAdjacentHTML('afterend', html); }
25 function getLS(key, parse) {
26 var x = window.localStorage.getItem(key)|| null;
27 return parse ? JSON.parse(x) : x;}
28 function setLS(key, value) {
29 if (typeof value == 'object') value = JSON.stringify(value);
30 window.localStorage.setItem(key, value);}
31 function PHSC(x) { return x.replace(/[&]/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>'); }
33 var __script__, wikitext;
34 var log = console.log;
37 var els = document.querySelectorAll('span._pmXmail');
38 var LinkFmt = '<a href="%u" class="mail">%t</a>';
40 for(var i=0; i<els.length; i++) {
41 var x = els[i].querySelector('span._t');
42 var txt = cb_mail(x.innerHTML);
43 var y = els[i].querySelector('span._m');
44 var url = cb_mail(y.innerHTML.replace(/^ *-> */, ''));
46 if(!url) url = 'mailto:'+txt.replace(/^mailto:/, '');
48 url = url.replace(/"/g, '%22').replace(/'/g, '%27');
49 var html = LinkFmt.replace(/%u/g, url).replace(/%t/g, txt);
50 els[i].innerHTML = html;
54 return x.replace( /<span class=(['"]?)_d\1>[^<]+<\/span>/ig, '.')
55 .replace( /<span class=(['"]?)_a\1>[^<]+<\/span>/ig, '@');
58 function is_toc_heading(el) {
59 if(el.offsetParent === null) {return false;} // hidden
60 if(el.closest('.notoc,.markup2')) {return false;}
65 if (el.offsetParent) {
68 } while (el = el.offsetParent);
74 if(h.id) {return h.id;} // %id=anchor%
75 var a = h.querySelector('a[id]'); // inline [[#anchor]]
76 if(a && a.id) {return a.id;}
77 var prev = h.previousElementSibling;
78 if(prev) { // [[#anchor]] before !!heading
79 var a = prev.querySelectorAll('a[id]');
82 if(last.id && ! last.nextElementSibling) {
83 var atop = posy(last) + last.offsetHeight;
85 if( Math.abs(htop-atop)<20 ) {
94 function inittoggle() {
95 var tnext = __script__.dataset.toggle;
96 if(! tnext) { return; }
98 if(! x.length) return;
99 for(var i=0; i<x.length; i++) togglenext(x[i]);
100 tap(tnext, togglenext);
101 tap('.pmtoggleall', toggleall);
103 function togglenext(z) {
104 var el = z.type == 'click' ? this : z;
105 var attr = el.dataset.pmtoggle=='closed' ? 'open' : 'closed';
106 el.dataset.pmtoggle = attr;
108 function toggleall(){
109 var curr = this.dataset.pmtoggleall;
110 if(!curr) curr = 'closed';
111 var toggles = dqsa('*[data-pmtoggle="'+curr+'"]');
112 var next = curr=='closed' ? 'open' : 'closed';
113 for(var i=0; i<toggles.length; i++) {
114 toggles[i].dataset.pmtoggle = next;
116 var all = dqsa('.pmtoggleall');
117 for(var i=0; i<all.length; i++) {
118 all[i].dataset.pmtoggleall = next;
123 if(dqs('.noPmTOC')) { return; } // (:notoc:) in page
124 var dtoc = __script__.dataset.pmtoc;
125 try {dtoc = JSON.parse(dtoc);} catch(e) {dtoc = false;}
126 if(! dtoc) { return; } // error
128 if(! dtoc.Enable || !dtoc.MaxLevel) { return; } // disabled
130 if(dtoc.NumberedHeadings) {
131 var specs = dtoc.NumberedHeadings.toString().split(/\./g);
132 for(var i=0; i<specs.length; i++) {
133 if(specs[i].match(/^[1AI]$/i)) numheadspec[i] = specs[i];
138 for(var i=1; i<=dtoc.MaxLevel; i++) {
141 if(dtoc.EnableQMarkup) query.push('p.question');
142 var pageheadings = wikitext.querySelectorAll(query.join(','));
143 if(!pageheadings.length) { return; }
145 var toc_headings = [ ];
146 var minlevel = 1000, hcache = [ ];
147 for(var i=0; i<pageheadings.length; i++) {
148 var h = pageheadings[i];
149 if(! is_toc_heading(h)) {continue;}
150 toc_headings.push(h);
152 if(! toc_headings.length) return;
154 var tocdiv = dqs('.PmTOCdiv');
155 var shouldmaketoc = ( tocdiv || (toc_headings.length >= dtoc.MinNumber && dtoc.MinNumber != -1)) ? 1:0;
156 if(!dtoc.NumberedHeadings && !shouldmaketoc) return;
158 for(var i=0; i<toc_headings.length; i++) {
159 var h = toc_headings[i];
160 var level = pf(h.tagName.substring(1));
161 if(! level) level = 6;
162 minlevel = Math.min(minlevel, level);
164 hcache.push([h, level, id]);
169 for(var i=0; i<hcache.length; i++) {
171 var actual_level = hc[1] - minlevel;
172 // if(actual_level>prevlevel+1) actual_level = prevlevel+1;
173 // prevlevel = actual_level;
175 var currnb = numberheadings(actual_level);
177 hc[2] = 'toc-'+currnb.replace(/\.+$/g, '');
180 if(dtoc.NumberedHeadings && currnb.length) adjab(hc[0], currnb+' ');
182 if(! shouldmaketoc) { continue; }
183 var txt = hc[0].textContent.trim().replace(/</g, '<');
184 var sectionedit = hc[0].querySelector('.sectionedit');
186 var selength = sectionedit.textContent.length;
187 txt = txt.slice(0, -selength);
190 html += '<a class="pmtoc-indent'+ actual_level
191 + '" href="#'+hc[2]+'">' + txt + '</a>\n';
192 if(dtoc.EnableBacklinks)
193 adjbe(hc[0], ' <a class="back-arrow" href="#_toc">↑</a>');
197 if(! shouldmaketoc) return;
199 html = "<b>"+dtoc.contents+"</b> "
200 +"[<input type='checkbox' id='PmTOCchk'><label for='PmTOCchk'>"
201 +"<span class='pmtoc-show'>"+dtoc.show+"</span>"
202 +"<span class='pmtoc-hide'>"+dtoc.hide+"</span></label>]"
203 +"<div class='PmTOCtable'>" + html + "</div>";
206 var wrap = "<div class='PmTOCdiv'></div>";
207 if(dtoc.ParentElement && dqs(dtoc.ParentElement)) {
208 adjab(dqs(dtoc.ParentElement), wrap);
211 adjbb(hcache[0][0], wrap);
213 tocdiv = dqs('.PmTOCdiv');
216 if(!tocdiv) return; // error?
217 tocdiv.className += " frame";
220 tocdiv.innerHTML = html;
222 if(getLS('closeTOC')) { dqs('#PmTOCchk').checked = true; }
223 aE('#PmTOCchk', 'change', function(e){
224 setLS('closeTOC', this.checked ? "close" : '');
227 var hh = location.hash;
229 var cc = document.getElementById(hh.substring(1));
230 if(cc) cc.scrollIntoView();
234 var numhead = [0, 0, 0, 0, 0, 0, 0];
235 var numheadspec = '1 1 1 1 1 1 1'.split(/ /g);
236 function numhead_alpha(n, upper) {
238 var alpha = '', mod, start = upper=='A' ? 65 : 97;
241 alpha = String.fromCharCode(start + mod) + '' + alpha;
246 function numhead_roman(n, upper) {
248 // partially based on http://blog.stevenlevithan.com/?p=65#comment-16107
249 var lst = [ [1000,'M'], [900,'CM'], [500,'D'], [400,'CD'], [100,'C'], [90,'XC'],
250 [50,'L'], [40,'XL'], [10,'X'], [9,'IX'], [5,'V'], [4,'IV'], [1,'I'] ];
252 for(var i=0; i<lst.length; i++) {
253 while(n>=lst[i][0]) {
258 return (upper == 'I') ? roman : roman.toLowerCase();
261 function numberheadings(n) {
262 if(n<numhead[6]) for(var j=numhead[6]; j>n; j--) numhead[j]=0;
266 for (var j=0; j<=n; j++) {
267 var curr = numhead[j];
268 var currspec = numheadspec[j];
269 if(currspec.match(/a/i)) { curr = numhead_alpha(curr, currspec); }
270 else if(currspec.match(/i/i)) { curr = numhead_roman(curr, currspec); }
277 function makesortable() {
278 if(! pf(__script__.dataset.sortable)) return;
279 var tables = dqsa('table.sortable,table.sortable-footer');
280 for(var i=0; i<tables.length; i++) {
281 // non-pmwiki-core table, already ready
282 if(tables[i].querySelector('thead')) continue;
284 tables[i].classList.add('sortable'); // for .sortable-footer
286 var thead = document.createElement('thead');
287 tables[i].insertBefore(thead, tables[i].firstChild);
289 var rows = tables[i].querySelectorAll('tr');
290 thead.appendChild(rows[0]);
291 var tbody = tables[i].querySelector('tbody');
293 tbody = tables[i].appendChild(document.createElement('tbody'));
294 for(var r=1; r<rows.length; r++) tbody.appendChild(rows[r]);
296 if(tables[i].className.match(/sortable-footer/)) {
297 var tfoot = tables[i].appendChild(document.createElement('tfoot'));
298 tfoot.appendChild(rows[rows.length-1]);
304 function mkdatasort(rows) {
305 var hcells = rows[0].querySelectorAll('th,td');
306 var specialsort = [], span;
307 for(var i=0; i<hcells.length; i++) {
308 sortspan = hcells[i].querySelector('.sort-number,.sort-number-us,.sort-date');
309 if(sortspan) specialsort[i] = sortspan.className;
311 if(! specialsort.length) return;
312 for(var i=1; i<rows.length; i++) {
313 var cells = rows[i].querySelectorAll('td,th');
315 for(var j=0; j<cells.length && j<specialsort.length; j++) {
316 if(! specialsort[j]) continue;
317 var t = cells[j].innerText, ds = '';
318 if(specialsort[j] == 'sort-number-us') {ds = t.replace(/[^-.\d]+/g, ''); }
319 else if(specialsort[j] == 'sort-number') {ds = t.replace(/[^-,\d]+/g, '').replace(/,/g, '.'); }
320 else if(specialsort[j] == 'sort-date') {ds = new Date(t).getTime(); }
321 if(ds) cells[j].setAttribute('data-sort', ds);
325 function libsortable(){
326 // adapted from Public Domain code by github.com/tofsjonas
327 function getValue(obj) {
328 obj = obj.cells[column_index];
329 return obj.getAttribute('data-sort') || obj.innerText;
331 function reclassify(element, cname) {
332 element.classList.remove('dir-u', 'dir-d');
333 if(cname) element.classList.add(cname);
336 document.addEventListener('click', function(e) {
337 if(e.target.closest('a')) return; // links
338 var element = e.target.closest('th');
339 if (! element) return;
340 var table = element.offsetParent;
341 if (!table.classList.contains('sortable')) return;
343 var cells = element.parentNode.cells;
344 for (var i = 0; i < cells.length; i++) {
345 if (cells[i] === element) {
348 reclassify(cells[i], '');
351 var cname = 'dir-d', reverse = false;
352 if (element.classList.contains(cname)) {
356 reclassify(element, cname);
357 var tbody = table.tBodies[0];
359 for(var r=0; r<tbody.rows.length; r++) rows.push(tbody.rows[r]);
361 rows.sort(function(x, y) {
362 var a = getValue(reverse? y:x),
363 b = getValue(reverse? x:y);
364 var c = a.localeCompare(b, undefined, {numeric: true, sensitivity: 'base'}),
366 return isNaN(d) ? c : d;
368 for (i = 0; i < rows.length; i++) {
369 tbody.appendChild(rows[i]);
374 function highlight_pre() {
375 if(!__script__.dataset.highlight) return;
376 if (typeof hljs == 'undefined') return;
378 var x = dqsa('.highlight,.hlt');
380 for(var i=0; i<x.length; i++) {
381 if(x[i].className.match(/(^| )(pm|pmwiki)( |$)/)) { continue;} // core highlighter
382 var pre = Array.from(x[i].querySelectorAll('pre,code'));
383 var n = x[i].nextElementSibling;
384 if (n && n.tagName == 'PRE') pre.push(n);
385 for(var j=0; j<pre.length; j++) {
386 pre[j].className += ' ' + x[i].className;
387 var varlinks = pre[j].querySelectorAll('a.varlink');
389 for(var v=0; v<varlinks.length; v++) {
390 vararray[varlinks[v].textContent] = varlinks[v].href;
393 if(pre[j].children) pre[j].textContent = pre[j].textContent;
395 hljs.highlightElement(pre[j]);
396 var hlvars = pre[j].querySelectorAll('span.hljs-variable');
397 for(var v=0; v<hlvars.length; v++) {
398 var hlvar = hlvars[v].textContent;
399 if(vararray.hasOwnProperty(hlvar)) {
400 hlvars[v].innerHTML = '<a class="varlink" href="'+vararray[hlvar]+'">'+hlvar+'</a>';
407 var Now, ltmode, daymonth, pagename;
408 function fmtLocalTime(stamp) {
409 var d = new Date(stamp*1000);
410 var tooltip = PHSC(d.toLocaleString());
413 if(Now-d < 24*3600000)
414 return [zpad(d.getHours()) + ':'+ zpad(d.getMinutes()), tooltip];
415 var D = zpad(d.getDate()), M = zpad(d.getMonth()+1);
416 var thedate = daymonth.replace(/%d/, D).replace(/%m/, M);
417 if(Now-d < 334*24*3600000) return [thedate, tooltip];
418 return [thedate + '/' + d.getFullYear(), tooltip];
421 function localTimes() {
422 ltmode = pf(__script__.dataset.localtimes);
425 var days = Math.floor(ltmode/10);
430 pagename = __script__.dataset.fullname;
431 var seenstamp = getLS('seenstamp', true);
432 if(!seenstamp) seenstamp = {};
433 var previous = seenstamp[pagename];
435 var times = dqsa('time[datetime]');
437 daymonth = new Date(2021, 11, 26, 17)
438 .toLocaleDateString().match(/26.*12/)? '%d/%m': '%m/%d';
440 var h72 = Now.getTime()/1000-days*24*3600;
442 for(var i=0; i<times.length; i++) {
443 var itemdate = new Date(times[i].dateTime);
444 var stamp = Math.floor(itemdate.getTime()/1000);
446 var li = times[i].closest('li');
447 if (!li || !li.innerHTML.match(/<\/a> \. \. \. /)) {
448 var x = fmtLocalTime(stamp);
449 times[i].innerHTML = x[0];
450 times[i].title = x[1] ? x[1]: itemdate.textContent;
453 var link = li.querySelector('a');
454 if(link.className.match(/createlinktext|wikilink|selflink/)) {
455 var diff = link.href + '?action=diff#diff' + stamp;
457 var h = link.href + '?action=diff&fmt=rclist';
458 adjbe(li, ' <b class="rcplus" data-url="'+h+'">+</b>');
461 // recent uploads, other? we want to know when the link becomes "visited"
462 else diff = link.href + '#diff' + stamp;
463 times[i].innerHTML = '<a href="'+diff+'">'+times[i].innerHTML+'</a>';
466 var difflinks = dqsa('a[href*="#diff"]'), diffcnt = 0;
467 for(var i=0; i<difflinks.length; i++) {
468 var link = difflinks[i];
469 if(link.hostname != location.hostname) continue;
470 var a = link.href.match(/[#]diff(\d+)$/);
473 stamp = parseInt(a[1]);
474 var x = fmtLocalTime(stamp);
476 link.innerHTML = x[0];
477 link.setAttribute('title', x[1] ? x[1]: link.textContent);
479 var par = link.closest('li');
481 par.insertBefore(link, par.firstChild);
482 adjae(link, " ");
483 if(previous && stamp>previous) par.classList.add('rcnew');
486 var pagetitle = dqs('#wikititle h1, h1.pagetitle');
488 var time = zpad(Now.getHours()) + ':'+ zpad(Now.getMinutes());
489 adjbe(pagetitle, ' <span class="rcreload" title="Click to reload">'+time+'</span>');
490 tap('.rcreload', function(){location.reload();});
492 aE('.rcnew', 'mouseup', function(e){
493 if(e.which == 2) this.classList.remove('rcnew');
495 tap('.rcplus', function(e){
497 plus.style.display = 'none';
498 var basehref = plus.dataset.url.replace(/&fmt=rclist/, '#diff')
499 .replace(/[&]/g, '&');
500 var fmt = '<p class="outdent"><a href="'+basehref+'%d" title="%T">%t</a> %s</p>\n';
501 fetch(plus.dataset.url)
502 .then(function(resp){return resp.text();})
503 .then(function(text){
504 var lines = text.split(/\n/g);
506 for(var i=0; i<lines.length; i++) {
507 a = lines[i].match(/^(\d+):(.*)$/);
509 var time = fmtLocalTime(pf(a[1]));
510 out += fmt.replace(/%d/, a[1]).replace(/%T/, time[1])
511 .replace(/%t/, time[0]).replace(/%s/, a[2]);
513 if(out) adjae(plus, out);
517 if(dqs('form[name="authform"]') || location.href.match(/action=/)) return;
518 seenstamp[pagename] = Math.floor(Now.getTime()/1000);
519 setLS('seenstamp', seenstamp);
523 __script__ = dqs('script[src*="pmwiki-utils.js"]');
524 wikitext = document.getElementById('wikitext');
525 var fn = [autotoc, inittoggle, PmXMail, localTimes, highlight_pre, makesortable];
526 fn.forEach(function(a){a();});
528 if( document.readyState !== 'loading' ) ready();
529 else window.addEventListener('DOMContentLoaded', ready);