Here's my best shot yet.
This one has proper tabbing support: tab cycles only through the select entries and the search field. You can use any number of tabbing sentinels; I used five, just in case the UI skips some when it gets behind. When you tab to the search field, the entire contents of it will be automatically selected.
Using the ≣ handle on the search bar, you can move it around, and change its opacity using the scroll wheel. The three states -- active, moving, deactive -- all have their own opacity.
The search is better modularized now; matching entries are highlighted in one function, dataset_highlight().
The search is just a plain case insensitive search. Search for nothing to clear an existing search.
Expanding the drop-down list for a select element does not seem to be possible for current Firefox versions. For Chrome, one could create and send a mousedown event, but I omitted that code since I don't have Chrome/chromium installed on this machine I'm using right now.
The Javascript code is now about 9800 bytes long, but about 2900 of that is just multiple consecutive spaces. Anyway, it's not going to matter much wrt. page load time, and you definitely do want it as part of the page (as opposed to saved in a separate file), to avoid all sorts of glitches.
Please test!
I'd be very interested to hear any comments on this, especially how intuitive or unnatural the interface feels to you. I've only tested it in Firefox 37 in x86-64 Linux.
Code:
<!DOCTYPE html>
<html>
<head>
<title> Tabbing test </title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style type="text/css">
#table {
border-collapse: collapse;
border: 1px solid #cccccc;
}
#table tr {
border: 0px none;
border-top: 1px solid #cccccc;
}
#table tr td {
padding: 0.1em 0.5em 0.1em 0.5em;
border: 0 none;
border-left: 1px solid #cccccc;
margin: 0.1em 0.5em 0.1em 0.5em;
}
#searchbar {
display: block;
position: fixed;
left: 0;
top: -1024;
padding: 0.2em 0.5em 0.2em 0.5em;
border: 1px solid #000000;
margin: 0.2em 0.5em 0.2em 0.5em;
background: #ffffff;
color: #000000;
}
#searchmove {
display: inline;
padding: 0.2em 0.5em 0.2em 0.5em;
border: 0px none;
margin: 0 0 0 0;
cursor: hand;
cursor: move;
}
</style>
<script type="text/javascript">
var tabbing = new(function() {
var self = this;
var bar = null;
var handle = null;
var form = null;
var input = null;
var found = null;
var dataset = null;
var sentinels_before_data = null;
var sentinels_after_data = null;
var sentinels_before_search = null;
var sentinels_after_search = null;
var opacity_shown = "1.0";
var opacity_hidden = "0.5";
var opacity_moving = "0.9";
var moving = null;
var shown = false;
self.show = function() {
bar.style.opacity = opacity_shown;
shown = true;
};
self.hide = function() {
bar.style.opacity = opacity_hidden;
shown = false;
};
self.focus_search = function(evt) {
var ev = evt || window.event;
self.show();
input.focus();
input.setSelectionRange(0, (input.value).length + 1);
if (ev)
ev.preventDefault();
return false;
};
self.focus_first = function(evt) {
var ev = evt || window.event;
if (found === null || found.length < 1) {
self.show();
input.focus();
} else {
self.hide();
found[0].target.focus();
}
if (ev)
ev.preventDefault();
return false;
};
self.focus_last = function(evt) {
var ev = evt || window.event;
if (found === null || found.length < 1) {
self.show();
input.focus();
} else {
self.hide();
found[found.length-1].target.focus();
}
if (ev)
ev.preventDefault();
return false;
};
self.select_focused = function(evt) {
var ev = evt || window.event;
/* Expand (ev.target) HTMLSelectElement somehow. */
};
self.dataset_highlight = function(i, is_a_match) {
if (is_a_match) {
if (i & 1) {
/* Matching entry, even row */
dataset[i].row.style.background = "#cce5ff";
dataset[i].row.style.border = "1px solid #99ccff";
} else {
/* Matching entry, odd row */
dataset[i].row.style.background = "#bfcfff";
dataset[i].row.style.border = "1px solid #99ccff";
}
} else {
if (i & 1) {
/* Normal entry, even row */
dataset[i].row.style.background = "#ffffff";
dataset[i].row.style.border = "";
} else {
/* Normal entry, odd row */
dataset[i].row.style.background = "#f7f7f7";
dataset[i].row.style.border = "";
}
}
};
self.do_find = function(evt) {
var ev = evt || window.event;
var phrase = String(input.value).toLowerCase();
var tabIndex = 1;
for (i = 0; i < sentinels_before_data.length; i++)
sentinels_before_data[i].tabIndex = tabIndex++;
found = [];
if (phrase.length > 0)
for (i = 0; i < dataset.length; i++)
if (dataset[i].text.contains(phrase)) {
dataset[i].target.tabIndex = tabIndex++;
self.dataset_highlight(i, true);
found[found.length] = dataset[i];
} else {
dataset[i].target.tabIndex = 0;
self.dataset_highlight(i, false);
}
else
for (i = 0; i < dataset.length; i++) {
dataset[i].target.tabIndex = tabIndex++;
self.dataset_highlight(i, false);
found[found.length] = dataset[i];
}
for (i = 0; i < sentinels_after_data.length; i++)
sentinels_after_data[i].tabIndex = tabIndex++;
for (i = 0; i < sentinels_before_search.length; i++)
sentinels_before_search[i].tabIndex = tabIndex++;
input.tabIndex = tabIndex++;
for (i = 0; i < sentinels_after_search.length; i++)
sentinels_after_search[i].tabIndex = tabIndex++;
self.focus_first(ev);
return false;
};
self.move_begin = function(evt) {
var ev = evt || window.event;
if (ev && moving === null) {
moving = { left: bar.offsetLeft,
top: bar.offsetTop,
clientX: ev.clientX,
clientY: ev.clientY };
bar.style.opacity = opacity_moving;
}
};
self.move_end = function() {
if (moving) {
bar.style.opacity = opacity_shown;
moving = null;
self.focus_search();
}
};
self.move = function(evt) {
if (moving) {
var ev = evt || window.event;
if (ev && ev.buttons && (ev.buttons & 1)) {
var left = moving.left + ev.clientX - moving.clientX;
var top = moving.top + ev.clientY - moving.clientY;
var maxLeft = window.innerWidth - bar.offsetWidth - (document.documentElement.offsetWidth - document.body.clientWidth);
var maxTop = window.innerHeight - bar.offsetHeight - (document.documentElement.offsetHeight - document.body.clientHeight) / 2;
if (left > maxLeft) left = maxLeft;
if (top > maxTop) top = maxTop;
if (left < 0) left = 0;
if (top < 0) top = 0;
bar.style.left = String(left) + "px";
bar.style.top = String(top) + "px";
(window.getSelection()).removeAllRanges();
ev.preventDefault();
ev.stopPropagation();
} else
self.move_end();
}
};
self.change_opacity = function(evt) {
var ev = evt || window.event;
var delta, opacity;
if (ev) {
if (ev.deltaY > 0)
delta = -0.1;
else
if (ev.deltaY < 0)
delta = +0.1;
opacity = parseFloat(bar.style.opacity) + delta;
if (opacity > 1.0) opacity = 1.0;
if (opacity < 0.1) opacity = 0.1;
bar.style.opacity = opacity;
if (moving)
opacity_moving = opacity;
else
if (shown)
opacity_shown = opacity;
else
opacity_hidden = opacity;
ev.preventDefault();
}
return false;
};
self.init = function() {
bar = document.getElementById("searchbar");
form = document.getElementById("searchform");
input = document.getElementById("search");
handle = document.getElementById("searchmove");
sentinels_before_data = document.getElementsByClassName("sentinel_before_data");
sentinels_after_data = document.getElementsByClassName("sentinel_after_data");
sentinels_before_search = document.getElementsByClassName("sentinel_before_search");
sentinels_after_search = document.getElementsByClassName("sentinel_after_search");
dataset = [];
var element = document.getElementsByClassName("name");
for (i = 0; i < element.length; i++) {
dataset[i] = { row: element[i].parentNode,
text: String(element[i].textContent).toLowerCase(),
target: element[i].parentNode.querySelector("select") };
if (dataset[i].target !== null)
dataset[i].target.addEventListener("focus", self.select_focused);
}
for (i = 0; i < sentinels_before_data.length; i++)
sentinels_before_data[i].addEventListener("focus", self.focus_search);
for (i = 0; i < sentinels_after_data.length; i++)
sentinels_after_data[i].addEventListener("focus", self.focus_search);
for (i = 0; i < sentinels_before_search.length; i++)
sentinels_before_search[i].addEventListener("focus", self.focus_last);
for (i = 0; i < sentinels_after_search.length; i++)
sentinels_after_search[i].addEventListener("focus", self.focus_first);
input.addEventListener("focus", self.show);
input.addEventListener("blur", self.hide);
form.addEventListener("submit", self.do_find);
self.do_find(null);
handle.addEventListener("wheel", self.change_opacity);
handle.addEventListener("mousedown", self.move_begin);
window.addEventListener("mousemove", self.move);
window.addEventListener("mouseup", self.move_end);
bar.style.display = "block";
bar.style.opacity = "1";
bar.style.top = String(window.innerHeight - bar.offsetHeight - (document.documentElement.offsetHeight - document.body.clientHeight) / 2) + "px";
self.focus_search();
};
});
document.addEventListener("DOMContentLoaded", tabbing.init);
</script>
</head>
<body>
<a class="sentinel_before_data" href="#"></a>
<a class="sentinel_before_data" href="#"></a>
<a class="sentinel_before_data" href="#"></a>
<a class="sentinel_before_data" href="#"></a>
<a class="sentinel_before_data" href="#"></a>
<form method="POST" action="#" accept-charset="UTF-8">
<table id="table">
<tr><td class="name"> 1 A B C D </td><td><select name="1"><option value="" selected="selected">...</option><option>A</option><option>B</option><option>C</option></select></td></tr>
<tr><td class="name"> 2 A B C D G </td><td><select name="2"><option value="" selected="selected">...</option><option>A</option><option>B</option><option>C</option></select></td></tr>
<tr><td class="name"> 3 A B C F G </td><td><select name="3"><option value="" selected="selected">...</option><option>A</option><option>B</option><option>C</option></select></td></tr>
<tr><td class="name"> 4 A B E F G </td><td><select name="4"><option value="" selected="selected">...</option><option>A</option><option>B</option><option>C</option></select></td></tr>
<tr><td class="name"> 5 A E F G </td><td><select name="5"><option value="" selected="selected">...</option><option>A</option><option>B</option><option>C</option></select></td></tr>
</table>
</form>
<a class="sentinel_after_data" href="#"></a>
<a class="sentinel_after_data" href="#"></a>
<a class="sentinel_after_data" href="#"></a>
<a class="sentinel_after_data" href="#"></a>
<a class="sentinel_after_data" href="#"></a>
<a class="sentinel_before_search" href="#"></a>
<a class="sentinel_before_search" href="#"></a>
<a class="sentinel_before_search" href="#"></a>
<a class="sentinel_before_search" href="#"></a>
<a class="sentinel_before_search" href="#"></a>
<div id="searchbar">
<form id="searchform" method="POST" action="#" accept-charset="UTF-8">
<div id="searchmove"> ≣ </div>
<input id="search" type="text" name="q" value="">
</form>
</div>
<a class="sentinel_after_search" href="#"></a>
<a class="sentinel_after_search" href="#"></a>
<a class="sentinel_after_search" href="#"></a>
<a class="sentinel_after_search" href="#"></a>
<a class="sentinel_after_search" href="#"></a>
</body>
</html>