dynamic-virtual-scroll/test.htm

231 lines
7.0 KiB
HTML

<div tabindex="1" id="scroll" style="overflow-y: scroll; width: 300px; position: relative; height: 300px; border: 1px solid gray;" onscroll="enqueue(scrollother)">
<div style="height: 1500px" id="content"></div>
<div id="realcontent" style="z-index: 1; width: 100%; position: absolute; overflow: hidden"></div>
</div>
<style>
.item { border: 1px solid gray; box-sizing: border-box; text-align: center; }
</style>
<script>
/*<!--*/
//http://stackoverflow.com/questions/11730596/non-linear-scrolling
var s = document.getElementById('scroll');
var c = document.getElementById('content');
var rc = document.getElementById('realcontent');
var minheight = 100;
var api = (function() {
var items = [];
for (var i = 0; i < 100; i++)
items.push(30+Math.round(Math.random()*120));
items = [31,109,107,97,140,133,49,69,123,43,137,128,31,101,120,148,119,30,31,37,43,119,105,106,91,148,36,85,76,97,148,31,53,126,138,51,85,65,53,78,39,49,133,49,103,146,143,148,115,49,55,98,44,84,57,127,98,98,104,116,150,82,123,62,134,109,89,71,36,50,146,146,53,35,86,143,86,138,39,81,72,106,88,117,127,97,127,147,102,42,124,99,32,51,84,45,106,139,140,69];
// Аналог API получения элементов
return [ items.length, function(start, count, callback)
{
setTimeout(function() { callback(items.slice(start, start+count)); }, 200);
} ]
})();
var totalItems = api[0];
var getItems = api[1];
c.style.height = (totalItems*minheight)+'px';
var lastFirst = 0;
var curFirst = 0, curOffset = 0, curCount = 0, curPos = null;
var pagesize = Math.floor(s.offsetHeight/30);
rc.parentNode.addEventListener('keydown', handlekey);
c.addEventListener('keydown', handlekey, true);
rc.addEventListener('keydown', handlekey, true);
//rc.parentNode.addEventListener('DOMMouseScroll', mouse_wheel);
//rc.parentNode.onmousewheel = mouse_wheel;
rc.parentNode.focus();
function mouse_wheel(event)
{
event = event||window.event;
var direction = ((event.wheelDelta) ? event.wheelDelta/120 : event.detail/-3) || false;
if (direction)
{
if (event.preventDefault)
event.preventDefault();
event.returnValue = false;
enqueue(scrollbypx, -direction * 30);
}
}
function handlekey(ev)
{
if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 33 || ev.keyCode == 34) // up, down, pgup, pgdown
{
var px;
if (ev.keyCode == 38) px = -30;
else if (ev.keyCode == 40) px = 30;
else if (ev.keyCode == 33) px = -270;
else if (ev.keyCode == 34) px = 270;
enqueue(scrollbypx, px);
}
else if (ev.keyCode == 36)
s.scrollTop = 0;
else if (ev.keyCode == 35)
s.scrollTop = s.scrollHeight-s.offsetHeight+1;
else
return;
ev.preventDefault();
}
var rq = [];
var loadedItems = [];
function enqueue(fn, arg)
{
if (!rq.length)
fn(arg);
else if (arg)
rq.push(function() { fn(arg) });
else if (rq[rq.length-1] != fn)
rq.push(fn);
}
function loadfrom(start, count)
{
console.log('load from '+start+': '+count+' items');
getItems(start, count, function(partItems)
{
var h, i;
for (i = 0; i < partItems.length; i++)
loadedItems[start+i] = partItems[i];
rq.length && (rq.shift())();
});
}
// скроллинг в пикселях (в пределах видимости - можно использовать для up/down, pgup/pgdown, колеса мыши)
function scrollbypx(px)
{
if (px < 0)
{
var cpx = px;
cpx += curOffset*loadedItems[curFirst];
var nf = curFirst;
while (nf > 0 && cpx < 0)
{
nf--;
if (!loadedItems[nf])
{
rq.unshift(function() { scrollbypx(px) });
loadfrom(nf+1 > pagesize ? nf+1-pagesize : 0, pagesize);
return;
}
cpx += loadedItems[nf];
}
newFirst = nf+(cpx/loadedItems[nf]);
}
else
{
px -= loadedItems[curFirst]*(1-curOffset);
var nf = curFirst;
while (nf < totalItems-1 && px > 0)
{
nf++;
px -= loadedItems[nf];
}
nfo = (px >= 0 ? loadedItems[nf] : px+loadedItems[nf]);
newFirst = nf+(nfo/loadedItems[nf]);
}
s.scrollTop = newFirst/lastFirst*(s.scrollHeight-s.offsetHeight+1);
rq.length && (rq.shift())();
}
function loadnext()
{
var h, i;
for (h = 0, i = curFirst; i < totalItems && h < s.offsetHeight; i++)
{
if (!loadedItems[i])
{
var start = i;
while (start < totalItems && !loadedItems[start] && start < i+pagesize)
start++;
start = start-pagesize;
start = start < 0 ? 0 : start;
rq.unshift(loadnext);
loadfrom(start, pagesize);
return;
}
h += (i == curFirst ? loadedItems[i]*(1-curOffset) : loadedItems[i]);
}
curCount = i-curFirst;
rq.length && (rq.shift())();
}
function renderitems()
{
var ih = '', h = 0;
for (var i = 0; i < curCount; i++)
{
ih += '<div class="item" style="height: '+loadedItems[curFirst+i]+'px">'+(curFirst+i)+'</div>';
h += loadedItems[curFirst+i];
}
// curPos, чтобы при плавном скролле вниз загруженное в середину не было видно
var t = Math.round(-loadedItems[curFirst]*curOffset+curPos);
rc.style.height = '1px'; // на случай, если браузеру вздумается где-то посередине перерисовать
rc.style.top = t+'px';
rc.style.height = (t+h < s.scrollHeight-1 ? h : s.scrollHeight-1-t)+'px';
rc.innerHTML = ih;
// Тут можно убить лишние элементы
var q0 = curFirst-pagesize;
q0 = q0 - q0%pagesize;
var q1 = curFirst+curCount+pagesize;
q1 = q1 + (pagesize-q1%pagesize);
var destroyed = 0;
for (var i in loadedItems)
{
if (i < q0 || i >= q1)
{
destroyed++;
delete loadedItems[i];
}
}
if (destroyed > 0)
console.log('destroy '+destroyed+' items');
rq.length && (rq.shift())();
}
// нелинейный скроллинг (в % от элементов)
function scrollother()
{
var i, h;
if (s.scrollTop == curPos)
return;
var newFirst = s.scrollTop / (s.scrollHeight-s.offsetHeight+1) * lastFirst;
if (newFirst > lastFirst)
newFirst = lastFirst;
curFirst = Math.floor(newFirst);
curOffset = newFirst-curFirst;
curPos = s.scrollTop;
rq.unshift(renderitems);
loadnext();
}
function initscroll()
{
var h = 0, i;
for (i = totalItems-1; i >= 0 && h < s.offsetHeight; i--)
{
if (!loadedItems[i])
{
rq.unshift(initscroll);
loadfrom(i+1-pagesize, pagesize);
return;
}
h += loadedItems[i];
}
lastFirst = i+1+(h-s.offsetHeight)/loadedItems[i+1];
rq.length && (rq.shift())();
}
rq.push(scrollother);
initscroll();
//-->
</script>