Skip to content

Instantly share code, notes, and snippets.

@dunhamsteve
Last active June 28, 2020 16:10
Show Gist options
  • Save dunhamsteve/3086981 to your computer and use it in GitHub Desktop.
Save dunhamsteve/3086981 to your computer and use it in GitHub Desktop.
decorator for iPad scrollable areas
// This code is public domain, share and enjoy.
window.ui = window.ui || {};
/** @constructor */
ui.ListView = function(element, collection) {
this.el = element;
this.cellHeight = 30;
this.collection = collection;
// Find the first element with class 'Template' and use as a row template
var tt = this.el.getElementsByClassName('Template');
if (tt.length > 0) {
var t = tt[0];
this.template = t;
t.parentNode.removeChild(t);
}
this.content = document.createElement('div');
this.el.style.overflow = 'auto';
this.el.appendChild(this.content);
this.cells = [];
this.listeners = [];
// Juggle rows on scroll
this.el.addEventListener('scroll', this.redraw.bind(this));
// Ensure we have enough rows.
window.addEventListener('resize', this.redraw.bind(this));
// Get us started
this.redraw();
};
/** Ensure we have enough physical lines to fill the screen */
ui.ListView.prototype.ensure = function(desired) {
while (this.cells.length < desired) {
var cell = new ui.Template(this, this.template);
cell.element.style.position = 'absolute';
cell.element.style.y = 0;
cell.element.style.x = 0;
cell.element.style.height = this.cellHeight + 'px';
cell.element.style.width = '100%';
this.content.appendChild(cell.element);
this.cells.push(cell);
}
};
/** Rerender the rows */
ui.ListView.prototype.redraw = function() {
var cellh = this.cellHeight;
var oheight = this.el.offsetHeight;
var top = this.el.scrollTop;
var topRow = ~~(top / cellh);
var len = this.collection.length;
this.content.style.height = (len * cellh) + 'px';
var slosh = 10;
var desiredRows = ~~(oheight / cellh + slosh);
this.ensure(desiredRows);
var start = Math.max(topRow - slosh / 2, 0);
var end = Math.min(len, start + desiredRows);
start = Math.max(0, end - desiredRows);
var rows = [];
for (var i = start; i < start + this.cells.length; i++) {
rows.push(i);
var cell = this.cells[i % this.cells.length];
if (i < len) {
var item = this.collection[i];
cell.setItem(item);
cell.setTop(cellh * i);
cell.element.style.display = 'block';
} else {
cell.element.style.display = 'none';
}
}
};
/** @constructor */
ui.Template = function(listView, node) {
this.listView = listView;
this.db = listView.collection;
var tasks = [];
this.tasks = tasks;
this.element = node.cloneNode(true);
// This recursive function sets up our "tasks" which fill in data from
function proc(node) {
var cc = node.childNodes;
for (var i = 0; i < cc.length; i++) {
var child = cc[i];
// TODO - Handle attributes and more complex text substitution.
// e.g. sequence of "text",key extracted from '{{title}} by {{author}}' or 'http://{{key}}_{{blah}}.jpg'
if (child.nodeType == 3) {
var m = child.textContent.match('{{(.*)}}');
if (m) {
var key = m[1];
tasks.push(function(doc) {
child.textContent = doc[key] || '';
});
}
} else {
proc(child);
}
}
}
proc(this.element);
};
ui.Template.prototype.setTop = function(top) {
this.element.style.top = top + 'px';
};
ui.Template.prototype.setItem = function(item) {
if (item != this.item) {
this.item = item;
this.tasks.forEach(function(func) { func(item); });
}
};
/** @constructor
*
* This mixes in a touch scroll handler for iPad. We can't use the native scrolling because it bounces the app.
*
* REVIEW - consider switching from overflow: auto to static positioning, so we can bounce/overflow our scrollable div.
*/
ui.ScrollController = function(el) {
this.el = el;
el.addEventListener('touchstart', this.touchstart.bind(this));
el.addEventListener('touchmove', this.touchmove.bind(this));
el.addEventListener('touchend', this.touchend.bind(this));
};
ui.ScrollController.prototype = {
touchstart: function (ev) {
if (this.animating)
this.endAnimation();
td = ev;
console.log('down',ev);
this.y = ev.changedTouches[0].clientY;
this.startY = this.y;
this.pos = 0;
this.prev = ev;
this.pstamp = ev.timeStamp;
this.velocity = undefined;
},
update: function(ev) {
var t = ev.changedTouches[0];
var deltaY = t.clientY - this.y;
this.el.scrollTop -= deltaY;
var deltaT = (ev.timeStamp - this.pstamp);
var velocity = deltaY / deltaT;
// weighted average
if (this.velocity !== undefined)
this.velocity = (2*velocity + this.velocity)/3;
else
this.velocity = velocity;
this.pos = (this.pos + 1 ) % 5;
this.prev = ev;
this.pstamp = ev.timeStamp;
if (this.timer)
clearInterval(this.timer);
this.timer = undefined;
},
touchmove: function (ev) {
var t = ev.changedTouches[0];
if (this.y) {
if (Math.abs(t.clientY - this.startY) > 10)
this.isScrolling = true;
if (this.isScrolling) {
this.update(ev);
this.y = t.clientY;
}
ev.preventDefault();
}
tm = ev;
},
touchend: function (ev) {
tu = ev;
this.y = undefined;
if (this.isScrolling) {
ev.preventDefault();
if (!isNaN(this.velocity)) {
this.startAnimation();
} else {
this.isScrolling = false;
}
}
console.log('up',ev, this.pos, this.velocity, this.data);
},
startAnimation: function() {
if (this.timer)
return;
this.animating = true;
this.foo = Date.now();
this.timer = setInterval(this.animate.bind(this), 10);
},
endAnimation: function() {
this.animating = false;
clearInterval(this.timer);
this.timer = undefined;
},
animate: function() {
if (!this.animating) {
clearInterval(this.timer);
return;
}
var now = Date.now();
var deltaY = this.velocity * (now-this.foo);
if (this.velocity > 0.02)
this.velocity -= .02;
else if (this.velocity < -.02)
this.velocity += .02;
this.el.scrollTop = ~~(this.el.scrollTop - deltaY);
this.foo = now;
if (Math.abs(deltaY) < 1) {
this.endAnimation();
this.isScrolling = false;
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment