www: Drop search autocomplete, but make search itself a bit more useful
This commit is contained in:
parent
fc9a19e7c4
commit
9a81147983
3 changed files with 31 additions and 252 deletions
71
www/index.pl
71
www/index.pl
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use v5.26;
|
use v5.26;
|
||||||
use warnings;
|
use warnings;
|
||||||
use TUWF ':html5_', ':xml';
|
use TUWF ':html5_', ':xml', 'uri_escape';
|
||||||
use POSIX 'ceil';
|
use POSIX 'ceil';
|
||||||
use List::Util 'uniq', 'min';
|
use List::Util 'uniq', 'min';
|
||||||
use SQL::Interp 'sql', 'sql_interp';
|
use SQL::Interp 'sql', 'sql_interp';
|
||||||
|
|
@ -206,14 +206,14 @@ sub framework_ {
|
||||||
|
|
||||||
html_ lang => 'en', sub {
|
html_ lang => 'en', sub {
|
||||||
head_ sub {
|
head_ sub {
|
||||||
link_ rel => 'stylesheet', type => 'text/css', href => '/man.css?5';
|
link_ rel => 'stylesheet', type => 'text/css', href => '/man.css?6';
|
||||||
title_ $o{title}.' - manned.org';
|
title_ $o{title}.' - manned.org';
|
||||||
};
|
};
|
||||||
body_ sub {
|
body_ sub {
|
||||||
header_ sub {
|
header_ sub {
|
||||||
a_ href => '/', 'Manned.org';
|
a_ href => '/', 'Manned.org';
|
||||||
form_ action => '/browse/search', method => 'get', sub {
|
form_ action => '/browse/search', method => 'get', sub {
|
||||||
input_ type => 'text', name => 'q', id => 'q', tabindex => 1;
|
input_ type => 'text', name => 'q', id => 'q', placeholder => 'ncdu, btrfs.8, git-*', value => $o{q}, tabindex => 1;
|
||||||
input_ type => 'submit', value => 'Search';
|
input_ type => 'submit', value => 'Search';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -226,7 +226,7 @@ sub framework_ {
|
||||||
};
|
};
|
||||||
span_ 'all manual pages are copyrighted by their respective authors.';
|
span_ 'all manual pages are copyrighted by their respective authors.';
|
||||||
};
|
};
|
||||||
script_ type => 'text/javascript', src => '/man.js', '';
|
script_ type => 'text/javascript', src => '/man.js?2', '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -592,57 +592,46 @@ TUWF::get '/info/about' => sub {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
# Very simple (and fast) prefix match.
|
|
||||||
sub search_man {
|
|
||||||
my($q, $limit) = @_;
|
|
||||||
|
|
||||||
my $sect = $q =~ s/^([0-9])\s+// || $q =~ s/\(([a-zA-Z0-9]+)\)$// ? $1 : '';
|
|
||||||
my $name = $q =~ s/^([a-zA-Z0-9,.:_-]+)// ? $1 : '';
|
|
||||||
|
|
||||||
return !$name ? [] : tuwf->dbAlli(
|
|
||||||
'SELECT name, section FROM mans WHERE', sql_and(
|
|
||||||
sql('lower(name) LIKE', \(escape_like(lc $name).'%')),
|
|
||||||
$sect ? sql('section ILIKE', \(escape_like(lc $sect).'%')) : (),
|
|
||||||
), 'ORDER BY name, section LIMIT', \$limit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
TUWF::get '/browse/search' => sub {
|
TUWF::get '/browse/search' => sub {
|
||||||
my $q = tuwf->reqGet('q')||'';
|
my $q = tuwf->reqGet('q')||'';
|
||||||
my $man = search_man $q, 150;
|
|
||||||
return tuwf->resRedirect("/$man->[0]{name}.$man->[0]{section}", 'temp') if @$man == 1;
|
|
||||||
|
|
||||||
framework_ title => 'Search results for '.$q, mainclass => 'searchres', sub {
|
my $name = $q;
|
||||||
h1_ 'Search results for '.$q;
|
my $sect = $name =~ s/^([0-9])\s+// || $name =~ s/\(([a-zA-Z0-9]+)\)$// || $name =~ s/\.([0-9][a-zA-Z0-9]*)$// ? $1 : '';
|
||||||
# Package search would also be useful.
|
($name,$sect) = ($sect,'') if !length $name;
|
||||||
p_ 'Note: This is just a simple case-insensitive prefix match on the man names. In the future we\'ll have more powerful search functionality. Hopefully.';
|
|
||||||
if(@$man) {
|
# Redirect if we have an exact match
|
||||||
|
my @sectsql = length $sect ? ('AND section =', \$sect) : ();
|
||||||
|
my $man = length $name && tuwf->dbRowi('SELECT name, section FROM mans WHERE name =', \$name, @sectsql);
|
||||||
|
return tuwf->resRedirect("/man/$man->{name}.$man->{section}", 'temp') if length $man->{name};
|
||||||
|
|
||||||
|
# Otherwise, do case-insensitive glob search
|
||||||
|
my $nameq = escape_like(lc $name) =~ tr/?*/_%/r;
|
||||||
|
my $lst = !length $nameq ? [] : tuwf->dbAlli('
|
||||||
|
SELECT name, section
|
||||||
|
FROM mans WHERE lower(name) LIKE', \$nameq, @sectsql, '
|
||||||
|
ORDER BY name, section
|
||||||
|
LIMIT 500');
|
||||||
|
|
||||||
|
framework_ title => 'Search results for '.$q, mainclass => 'searchres', q => $q, sub {
|
||||||
|
h1_ 'Search results for '.(length $sect ? "$name in section $sect" : $q);
|
||||||
|
if(@$lst) {
|
||||||
|
p_ 'Truncated to the first 500 results.' if @$lst >= 150;
|
||||||
ul_ sub {
|
ul_ sub {
|
||||||
li_ sub {
|
li_ sub {
|
||||||
a_ href => "/$_->{name}.$_->{section}", $_->{name};
|
a_ href => "/man/$_->{name}.$_->{section}", $_->{name};
|
||||||
small_ " $_->{section}";
|
small_ " $_->{section}";
|
||||||
} for @$man;
|
} for @$lst;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p_ 'No results :-(';
|
p_ 'No results :-(';
|
||||||
|
p_ sub {
|
||||||
|
a_ href => '?q='.uri_escape($name), 'Try again in other sections?' if length $sect;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
TUWF::get '/xml/search.xml' => sub {
|
|
||||||
my $q = tuwf->reqGet('q')||'';
|
|
||||||
my $man = search_man $q, 20;
|
|
||||||
|
|
||||||
tuwf->resHeader('Content-Type' => 'text/xml; charset=UTF-8');
|
|
||||||
xml;
|
|
||||||
tag 'results', sub {
|
|
||||||
tag 'item', id => "$_->{name}.$_->{section}", %$_, undef for @$man;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
# Object to represent the various URLs to a man page.
|
# Object to represent the various URLs to a man page.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ thead td { font-weight: bold }
|
||||||
header { border-bottom: 3px dotted #ccc; display: flex; justify-content: space-between; align-items: end; flex-wrap: wrap }
|
header { border-bottom: 3px dotted #ccc; display: flex; justify-content: space-between; align-items: end; flex-wrap: wrap }
|
||||||
header a { font: 24px "Arial", serif; font-weight: bold }
|
header a { font: 24px "Arial", serif; font-weight: bold }
|
||||||
header form { padding-bottom: 3px }
|
header form { padding-bottom: 3px }
|
||||||
header input[type=text] { width: 100px }
|
header input[type=text] { width: 200px }
|
||||||
|
|
||||||
footer { border-top: 3px dotted #ccc; color: #666; font-size: 12px; display: flex; justify-content: space-between; flex-wrap: wrap }
|
footer { border-top: 3px dotted #ccc; color: #666; font-size: 12px; display: flex; justify-content: space-between; flex-wrap: wrap }
|
||||||
|
|
||||||
|
|
|
||||||
210
www/man.js
210
www/man.js
|
|
@ -70,20 +70,6 @@ function tag() {
|
||||||
}
|
}
|
||||||
return el;
|
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 getText(obj) {
|
function getText(obj) {
|
||||||
return obj.textContent || obj.innerText || '';
|
return obj.textContent || obj.innerText || '';
|
||||||
}
|
}
|
||||||
|
|
@ -123,202 +109,6 @@ function setClass(obj, c, set) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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
|
|
||||||
(function(){
|
|
||||||
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');
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// The tabs on man pages
|
// The tabs on man pages
|
||||||
(function(){
|
(function(){
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue