This splits the 'package' table into 'packages' and 'package_versions', which should improve performance in some cases and simplify some future queries. Previously it wasn't very well defined whether packages were uniquely identified by (system, name) or by (system, category, name). This is now normalized to the latter form. This required changes to the package URLs to include the category.
521 lines
15 KiB
JavaScript
521 lines
15 KiB
JavaScript
/* The following functions are part of a minimal JS library I wrote for VNDB.org */
|
|
|
|
var expanded_icon = '▾';
|
|
var collapsed_icon = '▸';
|
|
|
|
var http_request = false;
|
|
function ajax(url, func, async) {
|
|
if(!async && http_request)
|
|
http_request.abort();
|
|
var req = (window.ActiveXObject) ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest();
|
|
if(req == null)
|
|
return alert("Your browser does not support the functionality this website requires.");
|
|
if(!async)
|
|
http_request = req;
|
|
req.onreadystatechange = function() {
|
|
if(!req || req.readyState != 4 || !req.responseText)
|
|
return;
|
|
if(req.status != 200)
|
|
return alert('Whoops, error! :(');
|
|
func(req);
|
|
};
|
|
url += (url.indexOf('?')>=0 ? ';' : '?')+(Math.floor(Math.random()*999)+1);
|
|
req.open('GET', url, true);
|
|
req.send(null);
|
|
}
|
|
|
|
function byId(n) {
|
|
return document.getElementById(n)
|
|
}
|
|
function byName(){
|
|
var d = arguments.length > 1 ? arguments[0] : document;
|
|
var n = arguments.length > 1 ? arguments[1] : arguments[0];
|
|
return d.getElementsByTagName(n);
|
|
}
|
|
function byClass() { // [class], [parent, class], [tagname, class], [parent, tagname, class]
|
|
var par = typeof arguments[0] == 'object' ? arguments[0] : document;
|
|
var t = arguments.length == 2 && typeof arguments[0] == 'string' ? arguments[0] : arguments.length == 3 ? arguments[1] : '*';
|
|
var c = arguments[arguments.length-1];
|
|
var l = byName(par, t);
|
|
var ret = [];
|
|
for(var i=0; i<l.length; i++)
|
|
if(hasClass(l[i], c))
|
|
ret[ret.length] = l[i];
|
|
return ret;
|
|
}
|
|
|
|
/* wrapper around DOM element creation
|
|
* tag('string') -> createTextNode
|
|
* tag('tagname', tag(), 'string', ..) -> createElement(), appendChild(), ..
|
|
* tag('tagname', { class: 'meh', title: 'Title' }) -> createElement(), setAttribute()..
|
|
* tag('tagname', { <attributes> }, <elements>) -> create, setattr, append */
|
|
function tag() {
|
|
if(arguments.length == 1)
|
|
return typeof arguments[0] != 'object' ? document.createTextNode(arguments[0]) : arguments[0];
|
|
var el = typeof document.createElementNS != 'undefined'
|
|
? document.createElementNS('http://www.w3.org/1999/xhtml', arguments[0])
|
|
: document.createElement(arguments[0]);
|
|
for(var i=1; i<arguments.length; i++) {
|
|
if(arguments[i] == null)
|
|
continue;
|
|
if(typeof arguments[i] == 'object' && !arguments[i].appendChild) {
|
|
for(attr in arguments[i]) {
|
|
if(attr == 'style')
|
|
el.setAttribute(attr, arguments[i][attr]);
|
|
else
|
|
el[ attr == 'class' ? 'className' : attr == 'for' ? 'htmlFor' : attr ] = arguments[i][attr];
|
|
}
|
|
} else
|
|
el.appendChild(tag(arguments[i]));
|
|
}
|
|
return el;
|
|
}
|
|
function addBody(el) {
|
|
if(document.body.appendChild)
|
|
document.body.appendChild(el);
|
|
else if(document.documentElement.appendChild)
|
|
document.documentElement.appendChild(el);
|
|
else if(document.appendChild)
|
|
document.appendChild(el);
|
|
}
|
|
function setContent() {
|
|
setText(arguments[0], '');
|
|
for(var i=1; i<arguments.length; i++)
|
|
if(arguments[i] != null)
|
|
arguments[0].appendChild(tag(arguments[i]));
|
|
}
|
|
function setText(obj, txt) {
|
|
if(obj.textContent != null)
|
|
obj.textContent = txt;
|
|
else
|
|
obj.innerText = txt;
|
|
}
|
|
|
|
function listClass(obj) {
|
|
var n = obj.className;
|
|
if(!n)
|
|
return [];
|
|
return n.split(/ /);
|
|
}
|
|
function hasClass(obj, c) {
|
|
var l = listClass(obj);
|
|
for(var i=0; i<l.length; i++)
|
|
if(l[i] == c)
|
|
return true;
|
|
return false;
|
|
}
|
|
function setClass(obj, c, set) {
|
|
var l = listClass(obj);
|
|
var n = [];
|
|
if(set) {
|
|
n = l;
|
|
if(!hasClass(obj, c))
|
|
n[n.length] = c;
|
|
} else {
|
|
for(var i=0; i<l.length; i++)
|
|
if(l[i] != c)
|
|
n[n.length] = l[i];
|
|
}
|
|
obj.className = n.join(' ');
|
|
}
|
|
|
|
|
|
// Dropdown Search
|
|
|
|
function dsInit(obj, url, trfunc, serfunc, retfunc, parfunc) {
|
|
obj.setAttribute('autocomplete', 'off');
|
|
obj.onkeydown = dsKeyDown;
|
|
obj.onblur = function() { setTimeout(function () { setClass(byId('ds_box'), 'hidden', true) }, 250) };
|
|
obj.ds_returnFunc = retfunc;
|
|
obj.ds_trFunc = trfunc;
|
|
obj.ds_serFunc = serfunc;
|
|
obj.ds_parFunc = parfunc;
|
|
obj.ds_searchURL = url;
|
|
obj.ds_selectedId = 0;
|
|
obj.ds_dosearch = null;
|
|
if(!byId('ds_box'))
|
|
addBody(tag('div', {id: 'ds_box', 'class':'hidden'}, tag('b', 'Loading...')));
|
|
}
|
|
|
|
function dsKeyDown(ev) {
|
|
var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
|
|
var obj = this;
|
|
|
|
if(c == 9) // tab
|
|
return true;
|
|
|
|
// do some processing when the enter key has been pressed
|
|
if(c == 13) {
|
|
var frm = obj;
|
|
while(frm && frm.nodeName.toLowerCase() != 'form')
|
|
frm = frm.parentNode;
|
|
if(frm) {
|
|
var oldsubmit = frm.onsubmit;
|
|
frm.onsubmit = function() { return false };
|
|
setTimeout(function() { frm.onsubmit = oldsubmit }, 100);
|
|
}
|
|
|
|
if(obj.ds_selectedId != 0)
|
|
obj.value = obj.ds_serFunc(byId('ds_box_'+obj.ds_selectedId).ds_itemData, obj);
|
|
if(obj.ds_returnFunc)
|
|
obj.ds_returnFunc(obj);
|
|
|
|
setClass(byId('ds_box'), 'hidden', true);
|
|
setContent(byId('ds_box'), tag('b', 'Loading...'));
|
|
obj.ds_selectedId = 0;
|
|
if(obj.ds_dosearch) {
|
|
clearTimeout(obj.ds_dosearch);
|
|
obj.ds_dosearch = null;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// process up/down keys
|
|
if(c == 38 || c == 40) {
|
|
var l = byName(byId('ds_box'), 'tr');
|
|
if(l.length < 1)
|
|
return true;
|
|
|
|
// get new selected id
|
|
if(obj.ds_selectedId == 0) {
|
|
if(c == 38) // up
|
|
obj.ds_selectedId = l[l.length-1].id.substr(7);
|
|
else
|
|
obj.ds_selectedId = l[0].id.substr(7);
|
|
} else {
|
|
var sel = null;
|
|
for(var i=0; i<l.length; i++)
|
|
if(l[i].id == 'ds_box_'+obj.ds_selectedId) {
|
|
if(c == 38) // up
|
|
sel = i>0 ? l[i-1] : l[l.length-1];
|
|
else
|
|
sel = l[i+1] ? l[i+1] : l[0];
|
|
}
|
|
obj.ds_selectedId = sel.id.substr(7);
|
|
}
|
|
|
|
// set selected class
|
|
for(var i=0; i<l.length; i++)
|
|
setClass(l[i], 'selected', l[i].id == 'ds_box_'+obj.ds_selectedId);
|
|
return true;
|
|
}
|
|
|
|
// perform search after a timeout
|
|
if(obj.ds_dosearch)
|
|
clearTimeout(obj.ds_dosearch);
|
|
obj.ds_dosearch = setTimeout(function() {
|
|
dsSearch(obj);
|
|
}, 500);
|
|
|
|
return true;
|
|
}
|
|
|
|
function dsSearch(obj) {
|
|
var box = byId('ds_box');
|
|
var val = obj.ds_parFunc ? obj.ds_parFunc(obj.value) : obj.value;
|
|
|
|
clearTimeout(obj.ds_dosearch);
|
|
obj.ds_dosearch = null;
|
|
|
|
// hide the ds_box div
|
|
if(val.length < 2) {
|
|
setClass(box, 'hidden', true);
|
|
setContent(box, tag('b', 'Loading...'));
|
|
obj.ds_selectedId = 0;
|
|
return;
|
|
}
|
|
|
|
// position the div
|
|
var ddx=0;
|
|
var ddy=obj.offsetHeight;
|
|
var o = obj;
|
|
do {
|
|
ddx += o.offsetLeft;
|
|
ddy += o.offsetTop;
|
|
} while(o = o.offsetParent);
|
|
|
|
box.style.position = 'absolute';
|
|
box.style.left = ddx+'px';
|
|
box.style.top = ddy+'px';
|
|
box.style.width = obj.offsetWidth+'px';
|
|
setClass(box, 'hidden', false);
|
|
|
|
// perform search
|
|
ajax(obj.ds_searchURL + encodeURIComponent(val), function(hr) {
|
|
dsResults(hr, obj);
|
|
});
|
|
}
|
|
|
|
function dsResults(hr, obj) {
|
|
var lst = hr.responseXML.getElementsByTagName('item');
|
|
var box = byId('ds_box');
|
|
if(lst.length < 1) {
|
|
setContent(box, tag('b', 'No results'));
|
|
obj.selectedId = 0;
|
|
return;
|
|
}
|
|
|
|
var tb = tag('tbody', null);
|
|
for(var i=0; i<lst.length; i++) {
|
|
var id = lst[i].getAttribute('id');
|
|
var tr = tag('tr', {id: 'ds_box_'+id, ds_itemData: lst[i]} );
|
|
setClass(tr, 'selected', obj.selectedId == id);
|
|
|
|
tr.onmouseover = function() {
|
|
obj.ds_selectedId = this.id.substr(7);
|
|
var l = byName(box, 'tr');
|
|
for(var j=0; j<l.length; j++)
|
|
setClass(l[j], 'selected', l[j].id == 'ds_box_'+obj.ds_selectedId);
|
|
};
|
|
tr.onmousedown = function() {
|
|
obj.value = obj.ds_serFunc(this.ds_itemData, obj);
|
|
if(obj.ds_returnFunc)
|
|
obj.ds_returnFunc();
|
|
setClass(box, 'hidden', true);
|
|
obj.ds_selectedId = 0;
|
|
};
|
|
|
|
obj.ds_trFunc(lst[i], tr);
|
|
tb.appendChild(tr);
|
|
}
|
|
setContent(box, tag('table', tb));
|
|
|
|
if(obj.ds_selectedId != 0 && !byId('ds_box_'+obj.ds_selectedId))
|
|
obj.ds_selectedId = 0;
|
|
}
|
|
|
|
|
|
|
|
/* What follows is specific to manned.org */
|
|
|
|
// Search box
|
|
searchRedir = false;
|
|
dsInit(byId('q'), '/xml/search.xml?q=', function(item, tr) {
|
|
tr.appendChild(tag('td', item.getAttribute('name'), tag('i', '('+item.getAttribute('section')+')')));
|
|
},
|
|
function(item) {
|
|
searchRedir = true;
|
|
location.href = '/'+item.getAttribute('name')+'.'+item.getAttribute('section').substr(0,1);
|
|
return item.getAttribute('name')+'('+item.getAttribute('section')+')';
|
|
},
|
|
function() {
|
|
if(!searchRedir) {
|
|
var frm=byId('q');
|
|
while(frm && frm.nodeName.toLowerCase() != 'form')
|
|
frm = frm.parentNode;
|
|
frm.submit();
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
|
|
// Efficiently pack an array of booleans into a string. (Uses something like
|
|
// base32) Note: The resulting array after decoding may have a few more
|
|
// elements than it had before decoding. These will be false.
|
|
var bsCharacters = "abcdefghijklmnopqrstuvwxyz234567";
|
|
|
|
function bsEncode(a) {
|
|
var v = 0;
|
|
var b = 0;
|
|
var r = '';
|
|
for(var i=0; i<a.length; i++) {
|
|
v = (v<<1) + (a[i]?1:0);
|
|
if(++b == 5) {
|
|
r += bsCharacters.charAt(v);
|
|
v = b = 0;
|
|
}
|
|
}
|
|
if(!a.length || b > 0)
|
|
r += bsCharacters.charAt(v<<(5-b));
|
|
return r;
|
|
}
|
|
|
|
function bsDecode(s) {
|
|
var a = [];
|
|
for(var i=0; i<s.length; i++) {
|
|
var n = s.charCodeAt(i);
|
|
n -= n >= 97 ? 97 : 24;
|
|
a.push(!!((n>>4)&1), !!((n>>3)&1), !!((n>>2)&1), !!((n>>1)&1), !!(n&1));
|
|
}
|
|
return a;
|
|
}
|
|
|
|
|
|
/* Structure of VARS.mans:
|
|
[
|
|
["System", "Full name", "short", [
|
|
[ "category", "package", "version", [
|
|
[ "section", "locale"||null ],
|
|
...
|
|
],
|
|
oldvisible // <- this is only set by JS
|
|
],
|
|
...
|
|
],
|
|
oldvisible // <- this is only set by JS
|
|
],
|
|
...
|
|
]
|
|
|
|
The godawful navigation code desperately needs a rewrite.
|
|
*/
|
|
|
|
navShowLocales = false;
|
|
navHasLocale = false;
|
|
|
|
function navCreate(nav) {
|
|
setText(nav, '');
|
|
|
|
view = navSerialize();
|
|
navHasLocale = false;
|
|
var dl = tag('dl', null);
|
|
|
|
for(var i=0; i<VARS.mans.length; i++) {
|
|
var sys = VARS.mans[i];
|
|
|
|
var isold = i > 0 && VARS.mans[i-1][0] == sys[0];
|
|
if(typeof sys[4] === 'undefined')
|
|
sys[4] = !isold;
|
|
|
|
var pkgnum = 0;
|
|
var dd = tag('dd', null);
|
|
|
|
if(sys[4])
|
|
for(var j=0; j<sys[3].length; j++)
|
|
if(navCreatePkg(nav, view, dd, sys, j))
|
|
pkgnum++;
|
|
|
|
if(!isold || sys[4])
|
|
dl.appendChild(tag('dt', sys[1],
|
|
isold || !VARS.mans[i+1] || VARS.mans[i+1][0] != sys[0] ? null : tag('a',
|
|
{href:'#', _sysn: sys[0], _sysi:i, 'class':'expand',
|
|
title: "Show/hide historical releases.",
|
|
onclick: function() {
|
|
for(var j=this._sysi+1; j<VARS.mans.length && VARS.mans[j][0] == this._sysn; j++)
|
|
VARS.mans[j][4] = !VARS.mans[j][4];
|
|
navCreate(nav);
|
|
return false
|
|
}}, VARS.mans[i+1][4] ? expanded_icon : collapsed_icon)
|
|
));
|
|
|
|
if(sys[4] && pkgnum > 0)
|
|
dl.appendChild(dd);
|
|
}
|
|
|
|
navCreateLinks(nav);
|
|
nav.appendChild(dl);
|
|
}
|
|
|
|
|
|
function navCreatePkg(nav, view, dd, sys, n) {
|
|
var pkg = sys[3][n];
|
|
|
|
var isold = n > 0 && sys[3][n-1][0] == pkg[0] && sys[3][n-1][1] == pkg[1];;
|
|
if(isold && !pkg[4])
|
|
return false;
|
|
|
|
var mannum = 0;
|
|
var pdd = tag('dd', null);
|
|
for(var i=0; i<pkg[3].length; i++) {
|
|
var man = pkg[3][i];
|
|
var txt = man[0] + (man[1] ? '.'+man[1] : '');
|
|
if(man[2] != VARS.hash && man[1])
|
|
navHasLocale = true;
|
|
if(man[2] == VARS.hash || (navShowLocales || !man[1])) {
|
|
if(i > 0)
|
|
pdd.appendChild(tag(' '));
|
|
pdd.appendChild(man[2] == VARS.hash ? tag('b', txt) : tag('a', {href:'/'+VARS.name+'/'+man[2]+'?v='+view}, txt));
|
|
mannum++;
|
|
}
|
|
}
|
|
|
|
if(mannum > 0) {
|
|
dd.appendChild(tag('dt', tag('a', {href:'/pkg/'+sys[2]+'/'+pkg[0]+'/'+pkg[1]+'/'+pkg[2]}, pkg[1]),
|
|
isold || !sys[3][n+1] || sys[3][n+1][0] != pkg[0] || sys[3][n+1][1] != pkg[1] ? null : tag('a',
|
|
{href:'#', _pkgn: pkg[0]+'-'+pkg[1], _pkgi:n, 'class':'expand',
|
|
title: 'Show/hide historical versions of this package',
|
|
onclick: function() {
|
|
for(var j=this._pkgi+1; j<sys[3].length && sys[3][j][0]+'-'+sys[3][j][1] == this._pkgn; j++)
|
|
sys[3][j][4] = !sys[3][j][4];
|
|
navCreate(nav);
|
|
return false
|
|
}}, sys[3][n+1][4] ? expanded_icon : collapsed_icon),
|
|
tag('i', pkg[0] + ' / ' + pkg[2])));
|
|
dd.appendChild(pdd);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
function navCreateLinks(nav) {
|
|
var t = (navShowLocales ? expanded_icon : collapsed_icon) + 'locales';
|
|
nav.appendChild(!navHasLocale ? tag('i', {'class':'global'}, t) : tag('a',
|
|
{ 'class': 'global',
|
|
href: '#',
|
|
title: 'Show/hide manuals in a non-standard locale.',
|
|
onclick: function() { navShowLocales = !navShowLocales; navCreate(nav); return false }
|
|
}, t
|
|
));
|
|
}
|
|
|
|
|
|
// Serializes the current navigation view into a short string. The string is a
|
|
// bsEncode()ed bit array, created as follows:
|
|
// array.push(navShowLocales);
|
|
// for(each system that has an expand button)
|
|
// array.push(is the butten expanded or not);
|
|
// for(each system)
|
|
// for(each package that has an expand button)
|
|
// array.push(is the button expanded or not);
|
|
// Obviously, this means that the serialized view depends on the number and
|
|
// order of systems and packages. The order is stable, the number may change
|
|
// with database updates.
|
|
function navSerialize() {
|
|
var a = [navShowLocales];
|
|
for(var i=0; i<VARS.mans.length; i++)
|
|
if(i+1 < VARS.mans.length && VARS.mans[i+1][0] == VARS.mans[i][0] && (i == 0 || VARS.mans[i-1][0] != VARS.mans[i][0]))
|
|
a.push(!!VARS.mans[i+1][4]);
|
|
for(var i=0; i<VARS.mans.length; i++)
|
|
for(var j=0; j<VARS.mans[i][3].length; j++)
|
|
if(j+1 < VARS.mans[i][3].length && VARS.mans[i][3][j+1][0] == VARS.mans[i][3][j][0] && VARS.mans[i][3][j+1][1] == VARS.mans[i][3][j][1] && (j == 0 || VARS.mans[i][3][j-1][0] != VARS.mans[i][3][j][0] || VARS.mans[i][3][j-1][1] != VARS.mans[i][3][j][1]))
|
|
a.push(!!VARS.mans[i][3][j+1][4]);
|
|
return bsEncode(a).replace(/(.)a+$/, '$1');
|
|
}
|
|
|
|
// And the reverse of the above.
|
|
function navLoad(s) {
|
|
var a = bsDecode(s);
|
|
navShowLocales = !!a.shift();
|
|
for(var i=0; i<VARS.mans.length; i++)
|
|
if(i > 0 && VARS.mans[i-1][0] == VARS.mans[i][0])
|
|
VARS.mans[i][4] = i > 1 && VARS.mans[i-2][0] == VARS.mans[i-1][0] ? VARS.mans[i-1][4] : !!a.shift();
|
|
for(var i=0; i<VARS.mans.length; i++)
|
|
for(var j=0; j<VARS.mans[i][3].length; j++)
|
|
if(j > 0 && VARS.mans[i][3][j-1][0] == VARS.mans[i][3][j][0] && VARS.mans[i][3][j-1][1] == VARS.mans[i][3][j][1])
|
|
VARS.mans[i][3][j][4] = j > 1 && VARS.mans[i][3][j-2][0] == VARS.mans[i][3][j-1][0] && VARS.mans[i][3][j-2][1] == VARS.mans[i][3][j-1][1] ? VARS.mans[i][3][j-1][4] : !!a.shift();
|
|
}
|
|
|
|
|
|
if(byId('nav')) {
|
|
navLoad(VARS.view||'');
|
|
navCreate(byId('nav'));
|
|
}
|
|
|
|
|
|
|
|
// The "more..." links on the homepage.
|
|
if(byId('systems')) {
|
|
var f = function() {
|
|
var l = byName(this.parentNode, 'a', 'hidden');
|
|
for(var i=0; i<l.length; i++)
|
|
setClass(l[i], 'hidden', false);
|
|
setClass(this, 'hidden', true);
|
|
return false
|
|
};
|
|
var l = byClass(byId('systems'), 'a', 'more');
|
|
for(var i=0; i<l.length; i++)
|
|
l[i].onclick = f;
|
|
}
|