MediaWiki:Mobile.js: Difference between revisions

From the change wiki
No edit summary
Tags: Mobile edit Mobile web edit
No edit summary
Tag: Reverted
Line 384: Line 384:
parseLevels();
parseLevels();
parseWordChangers();
parseWordChangers();
//==Bug fixes==
mw.loader.using('mobile.site.styles'); // [[Mediawiki:Mobile.css]] doesn't load without this

Revision as of 17:03, 23 March 2024

/* All JavaScript here will be loaded for users of the mobile site */



//==Expandables and calculations==

var _calc3 = ''; // string to be sent to calc3.php ajax
var _popupLastOpened = null; // element
const _popupClasses = ['ppContent','dp','dpVar'];
function closePopup() {
 for (var i=0; i<_popupClasses.length; i++) {
  var e = document.getElementsByClassName(_popupClasses[i]);
  for (var j=0; j<e.length; j++) {
   e[j].style.display = 'none';
  }
 } _popupLastOpened = null;
}
function togglePopup(element) {
 if (element == _popupLastOpened) {
  element.style.display = 'none';
  _popupLastOpened = element.parentNode;
 }
 else {
  closePopup();
  _popupLastOpened = element;
  // Show the element and any ancestors that are of a popup class
  var a = [];
  for (var p=element; p; p=p.parentNode) {
   if (_popupClasses.indexOf(p.className) >= 0) {
    p.style.display = 'initial';
    a.push(p);
   }
  }
  // Fix squished popups (needed for mobile)
  var fix = 0;
  for (var i=0; i<a.length; i++) {
   if (a[i].clientWidth < a[i].clientHeight && a[i].clientHeight > 50) fix += 2;
   if (fix) {
    a[i].style.left = a[i].style.right = '0.5em';
    if ((i==0 && a.length>1) || a[i].className == 'ppContent') a[i].style.marginTop = '1em';
    fix--;
   }
  }
 }
}

function showOnlyNext(ev) { // expand inline
 ev.currentTarget.style.display = "none";
 ev.currentTarget.nextElementSibling.style.display = "initial";
 ev.stopPropagation();
}
function showOnlyPrev(ev) { // collapse inline
 ev.currentTarget.style.display = "none";
 ev.currentTarget.previousElementSibling.style.display = "initial";
 ev.stopPropagation();
}
function tpNext(ev) { // show popup
 togglePopup(ev.currentTarget.nextElementSibling);
 ev.stopPropagation();
}
function tpSelf(ev) { // hide popup
 togglePopup(ev.currentTarget);
 ev.stopPropagation();
}

function dpInfo(o) { // for calculator: display a datapoint's info
 var id = o.innerHTML;
 var calc = o.parentElement;
 var elem = document.getElementById('dp-'+id);
 if (elem) {
  calc.appendChild(elem);
  togglePopup(elem);
 }
}


function parseExpandables() { // init
 // Expandable text and popups
 var e = document.getElementsByClassName('forMore');
 for (var i=0; i<e.length; i++) {
  var next = e[i].nextElementSibling;
  if (next.className=="xpContent" || next.classList.contains("xpContent")) {
   e[i].onclick = showOnlyNext;
   next.onclick = showOnlyPrev;
  } else
  if (next.className=="ppContent" || next.classList.contains("ppContent")) {
   e[i].onclick = tpNext;
   next.onclick = tpSelf;
  }
 }
 // Expandable bullet list
 e = document.getElementsByClassName("expandable-bullets");
 for (var i=0; i<e.length; i++) {
  var f = e[i].getElementsByTagName("ul");
  for (var j=1; j<f.length; j++) {
   f[j].classList.add("mw-collapsible");
   f[j].classList.add("mw-collapsed");
  }
 }
}


function parseCalculations() { // init
 // Local datapoints and calculations
 var e = document.getElementsByClassName('dp');
 for (var i=0; i<e.length; i++) {
  var id = e[i].children[0].innerHTML.trim().split('\n')[0];
  var val= e[i].children[1].innerHTML.split('\n').join(' ');
  var redundant = document.getElementById('dp-'+id);
  if (redundant) redundant.id = ''; // XXX: This can only handle the case 'same identifier, same value'. It can't handle 'same identifier, different value' - at least 1 calculation would show up wrong. But to fix that, we would need a huge refactor - I don't think it's worthwhile. Instead, let's just warn users not to let their datapoints conflict with each other.
  e[i].id = 'dp-'+id;
  e[i].onclick = closePopup;
  _calc3 += id+' = '+val+'\n';
 }
 e = document.getElementsByClassName('calc');
 for (var i=0; i<e.length; i++) {
  var v = e[i].children[2] && e[i].children[2].innerHTML.trim().split('\n')[0]; // variable
  if (v) {
   e[i].id = 'calc-'+v; // XXX: If the user sets the same variable more than once (iterative math), it'll break the 'From previous calculation' link. To fix this, we would have to edit both this script and calc3.php.
   var n = document.createElement('div');
   n.className = 'dpVar'; n.id = 'dp-'+v;
   n.innerHTML = "<div class='dp0'>"+v+"</div><div class='dp2'>From <a href='#calc-"+v+"'>previous calculation</a></div>";
   n.onclick = closePopup;
   e[i].parentElement.insertBefore(n, e[i]);
   _calc3 += v+' = ';
  }
  _calc3 += e[i].children[0].innerHTML.split('\n').join(' '); // expression
  if (e[i].children[1]) _calc3 += ' <> '+e[i].children[1].innerHTML.split('\n').join(' '); // units wanted
  _calc3 += '\n';
  e[i].innerHTML = 'Loading...';
 }
 var xhttp = new XMLHttpRequest();
 xhttp.onreadystatechange = function() {
  if (this.readyState==4 && this.status==200) {
   var l=0;
   var lines = this.responseText.split('\n');
   var e = document.getElementsByClassName('dp');
   for (var i=0; i<e.length; i++, l++) e[i].children[1].innerHTML = lines[l].split('<br')[0]; // update 'value' to contain hyperlinks
   e = document.getElementsByClassName('calc');
   for (var i=0; i<e.length; i++, l++) e[i].innerHTML = lines[l]; // equation
  }
 };
 xhttp.open("POST", "/calc/calc3.php?input="+encodeURIComponent(_calc3), true)
 try { xhttp.send(); } catch(error) { /* AJAX URL not found */ }

 // External calculation links
 e = document.getElementsByClassName('ecalc');
 for (var i=0; i<e.length; i++) {
  var str = "<a href='/calc/calc2.php";
  str += "?a="    +encodeURIComponent(e[i].children[0].innerHTML);
  str += "&b="    +encodeURIComponent(e[i].children[1].innerHTML);
  str += "&title="+encodeURIComponent(e[i].children[2].innerHTML);
  str += "' target='_blank' class='forMore'>[=] &#128279;</a>";
  e[i].innerHTML = str;
  e[i].style.display = 'initial';
 }
}




//==File graphs==

// Functions for drag-and-drop of file graph nodes:
function allowDrop(ev) { ev.preventDefault(); }
function drag(ev) {
 var r = ev.target.getBoundingClientRect();
 ev.dataTransfer.setData("text", ev.target.id + ','
                              + (ev.clientX - 0.5*(r.left+r.right)) + ','
                              + (ev.clientY - r.top));
}
function drop(ev) {
 ev.preventDefault();
 var fg = ev.target;
 while (fg && fg.className != 'fileGraph') fg = fg.parentNode;
 if (fg) {
  var d = ev.dataTransfer.getData("text").split(',');
  var n = document.getElementById(d[0]);
  var r = fg.getBoundingClientRect();
  n.style.left = (ev.clientX - r.left - parseFloat(d[1]))+'px';
  n.style.top  = (ev.clientY - r.top  - parseFloat(d[2]))+'px';
  updateFgConnect();
  updateWikiText(n);
 }
}

// Function to draw the connections in the file graph
function updateFgConnect() {
 const connectionLabels = ['- - - -','generates','is input for','contributes to'];
 const connectionColors = ['#333','#F06','#666','#960'];
 e = document.getElementsByClassName('fgConnect');
 for (var i=0; i<e.length; i++) {
  var a = e[i].title.split(',');
  var from = document.getElementById('fgNode-'+a[0].trim());
  var to   = document.getElementById('fgNode-'+a[1].trim());
  var type = (a[2] && a[2].trim()) || 0;
  var label;
  if (connectionLabels[type] && connectionColors[type]) {
   label = connectionLabels[type];
   e[i].style.color = e[i].style.borderColor = connectionColors[type];
  } else label = type;
  var fs = window.getComputedStyle(from);
  var ts = window.getComputedStyle(to);
  var x1 = parseFloat(fs.getPropertyValue('left'));
  var y1 = parseFloat(fs.getPropertyValue('top'));
  var x2 = parseFloat(ts.getPropertyValue('left'));
  var y2 = parseFloat(ts.getPropertyValue('top'));
  var angle = Math.atan((y2-y1)/(x2-x1))*180/Math.PI;
  var len = Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
  if (x2 >= x1) {
   e[i].innerHTML = '--&gt;&gt; '+label+' &gt;&gt;';
   e[i].style.left = x1+'px';
   e[i].style.top = y1+'px';
  }
  else {
   e[i].innerHTML = '&lt;&lt; '+label+' &lt;&lt;--';
   e[i].style.left = x2+'px';
   e[i].style.top = y2+'px';
  }
  e[i].style.paddingLeft=e[i].style.paddingRight = '0px';
  e[i].style.transform = 'none';
  e[i].style.display = 'initial';
  var padding = 0.5*(len - e[i].offsetWidth);
  e[i].style.paddingLeft=e[i].style.paddingRight = padding+'px';
  e[i].style.transform = 'rotate('+angle+'deg)';
 }
}

// Function for mediawiki 'preview' mode only:
//   Alters the wikitext in the editor: Edits the coordinates of a file-graph node after dragging it.
//   Warning: if you don't write your wikitext properly, you may get undefined results!
var _doUpdateWt=1;
function updateWikiText(fgNode) {
 if (_doUpdateWt) {
  var prefix = document.title.substr(0,8);
  var target = document.getElementById('wpTextbox1');
  if (target && (prefix=='Editing ' || prefix=='Creating')) {
   if (_doUpdateWt==1) {
    if (confirm('Update coordinates in wikitext?')) _doUpdateWt=2;
    else _doUpdateWt=0;
   }
   if (_doUpdateWt==2) {
    // get data to generate the new coordinates string
    var s = window.getComputedStyle(fgNode);
    var x = parseFloat(s.getPropertyValue('left'));
    var y = parseFloat(s.getPropertyValue('top'));
    var graph = fgNode.parentNode;
    s = window.getComputedStyle(graph);
    var em = parseFloat(s.getPropertyValue('font-size'));
    // generate strings
    var search = '{{fg-node|'+fgNode.id.substr(7)+'|';
    var replace = parseInt(100*x/graph.offsetWidth) + '%|' + (y/em+0.5).toFixed(1) + 'em'; // the new coordinates string
    // parse wikitext to find the place to insert
    var input = target.value;
    var i, j=0, len=input.length, sl=search.length;
    for (i=0; i<len; i++) {
     var c = input[i];
     if (c==' '||c=='\t'||c=='\n'||c=='\r'||c=='\v') continue;
     if (c == search[j]) {
      j++;
      if (j >= sl) break;
     }
     else j=0;
    }
    var start = ++i; while (i<len && input[i] != '|') i++;
    i++;             while (i<len && input[i] != '|') i++;
    // insert the new coordinates string
    if (start<len && i<len) target.value = input.substr(0,start) + replace + input.substr(i);
   }
  }else _doUpdateWt=0;
 }
}


function parseFileGraphs() { // init
 updateFgConnect();
 window.addEventListener('resize', updateFgConnect);
 // make file graph nodes draggable
 var e = document.getElementsByClassName('fileGraph');
 for (var i=0; i<e.length; i++) {
  e[i].id = 'fileGraph'+i;
  e[i].ondrop = drop;
  e[i].ondragover = allowDrop;
 }
 e = document.getElementsByClassName('fgNode');
 for (var i=0; i<e.length; i++) {
  e[i].draggable = true;
  e[i].ondragstart = drag;
  var c = e[i].children;
  for (var j=0; j<c.length; j++) {
   if (c[j].tagName=='A') c[j].draggable = false; // override hyperlink drag
  }
 }
}




//==Considerations table==

function parseConsiderations() { // init
 var elem = document.getElementById("considerations-table");
 if (elem) {
  var e = document.getElementsByClassName("consideration-summary");
  var str = "<table class='wikitable'>";
  for (var i=0; i<e.length; i++) {
   var topic = e[i].previousElementSibling ? 
    e[i].previousElementSibling.getElementsByClassName("mw-headline")[0] :
    e[i].parentElement.previousElementSibling.getElementsByClassName("mw-headline")[0]; // DIFFERENT FROM Common.js
   str += "<tr><td><a href='#"+topic.id+"'>"+topic.innerHTML+"</a></td>";
   if     (e[i].title=="bad") str += "<td style='background:#F65'>";
   else if(e[i].title=="good")str += "<td style='background:#0FB'>";
   else str += "<td>";
   str += e[i].innerHTML;
   str += "</td></tr>";
   //e[i].style.display = "none";
  }
  str += "</table>";
  elem.innerHTML = str;
 }
}




//==Text levels==
function parseLevels() { // init
 var e = document.getElementsByClassName("levels");
 for (var i=0; i<e.length; i++) {
  var size = parseFloat(window.getComputedStyle(e[i]).getPropertyValue("font-size")) || 16;
  var lines = e[i].innerHTML.split('\n');
  var str = "";
  for (var j=0; j<lines.length; j++) {
   var n=0; while (lines[j][n]==' ') n++;
   var k = Math.exp(-0.16*n);
   str += "<div style='font-size:"  +(1.4*size*k+6)
                 +"px; margin-left:"+(size*(n*0.5 + 12-12*k))
                 +"px; font-family:"+((n%2)?"serif":"sans")
                 +  "; color:RGB(0,"+(255-255*k)+",0)"
                 +  "; padding:0.15em 0; line-height:1.1em'>"
       +(lines[j].substr(n) || (j==lines.length-1 ? "":"&nbsp;"))+"</div>";
  }
  e[i].innerHTML = str;
 }
}




//==Word changers==
function changeWords(ev) { // to next option
 var hide = ev.currentTarget;
 if(!hide || !hide.classList.contains("altOuter")) return;
 var show = hide.nextElementSibling;
 if(!show || !show.classList.contains("altOuter") || !show.innerHTML) {
  var find = hide;
  do { // seek back to the first of list
   show = find;
   find = find.previousElementSibling;
  } while (find && find.classList.contains("altOuter"));
 }
 hide.style.display = "none";
 show.style.display = "initial";
}
function parseWordChangers() { // init
 var e = document.getElementsByClassName("altOuter");
 for (var i=0; i<e.length; i++) {
  var str = e[i].innerHTML;
  if (str) {
   e[i].innerHTML = "&#8597;<span class='altInner'>"+str+"</span>&#8597;";
   e[i].onclick = changeWords;
  }
 }
}




//==Init when page loads==
parseExpandables();
parseCalculations();
parseFileGraphs();
parseConsiderations();
parseLevels();
parseWordChangers();


//==Bug fixes==
mw.loader.using('mobile.site.styles'); // [[Mediawiki:Mobile.css]] doesn't load without this