|
/**
|
|
A control that displays a scrolling list of rows, suitable for displaying
|
|
very large lists. _enyo.List_ is optimized such that only a small portion of
|
|
the list is rendered at a given time. A flyweight pattern is employed, in
|
|
which controls placed inside the list are created once, but rendered for
|
|
each list item. For this reason, it's best to use only simple controls in
|
|
a List, such as <a href="#enyo.Control">enyo.Control</a> and
|
|
<a href="#enyo.Image">enyo.Image</a>.
|
|
|
|
A List's _components_ block contains the controls to be used for a single
|
|
row. This set of controls will be rendered for each row. You may customize
|
|
row rendering by handling the _onSetupItem_ event.
|
|
|
|
Events fired from within list rows contain the _index_ property, which may
|
|
be used to identify the row from which the event originated.
|
|
|
|
The controls inside a List are non-interactive. This means that calling
|
|
methods that would normally cause rendering to occur (e.g., _setContent_)
|
|
will not do so. However, you can force a row to render by calling
|
|
_renderRow(inRow)_.
|
|
|
|
In addition, you can force a row to be temporarily interactive by calling
|
|
_prepareRow(inRow)_. Call the _lockRow_ method when the interaction is
|
|
complete.
|
|
|
|
For more information, see the documentation on
|
|
[Lists](https://github.com/enyojs/enyo/wiki/Lists)
|
|
in the Enyo Developer Guide.
|
|
*/
|
|
enyo.kind({
|
|
name: "enyo.List",
|
|
kind: "Scroller",
|
|
classes: "enyo-list",
|
|
published: {
|
|
/**
|
|
The number of rows contained in the list. Note that as the amount of
|
|
list data changes, _setRows_ can be called to adjust the number of
|
|
rows. To re-render the list at the current position when the count
|
|
has changed, call the _refresh_ method.
|
|
*/
|
|
count: 0,
|
|
/**
|
|
The number of rows to be shown on a given list page segment.
|
|
There is generally no need to adjust this value.
|
|
*/
|
|
rowsPerPage: 50,
|
|
/**
|
|
If true, renders the list such that row 0 is at the bottom of the
|
|
viewport and the beginning position of the list is scrolled to the
|
|
bottom
|
|
*/
|
|
bottomUp: false,
|
|
//* If true, multiple selections are allowed
|
|
multiSelect: false,
|
|
//* If true, the selected item will toggle
|
|
toggleSelected: false,
|
|
//* If true, the list will assume all rows have the same height for optimization
|
|
fixedHeight: false
|
|
},
|
|
events: {
|
|
/**
|
|
Fires once per row at render time, with event object:
|
|
_{index: <index of row>}_
|
|
*/
|
|
onSetupItem: ""
|
|
},
|
|
handlers: {
|
|
onAnimateFinish: "animateFinish"
|
|
},
|
|
//* @protected
|
|
rowHeight: 0,
|
|
listTools: [
|
|
{name: "port", classes: "enyo-list-port enyo-border-box", components: [
|
|
{name: "generator", kind: "FlyweightRepeater", canGenerate: false, components: [
|
|
{tag: null, name: "client"}
|
|
]},
|
|
{name: "page0", allowHtml: true, classes: "enyo-list-page"},
|
|
{name: "page1", allowHtml: true, classes: "enyo-list-page"}
|
|
]}
|
|
],
|
|
create: function() {
|
|
this.pageHeights = [];
|
|
this.inherited(arguments);
|
|
this.getStrategy().translateOptimized = true;
|
|
this.bottomUpChanged();
|
|
this.multiSelectChanged();
|
|
this.toggleSelectedChanged();
|
|
},
|
|
createStrategy: function() {
|
|
this.controlParentName = "strategy";
|
|
this.inherited(arguments);
|
|
this.createChrome(this.listTools);
|
|
this.controlParentName = "client";
|
|
this.discoverControlParent();
|
|
},
|
|
rendered: function() {
|
|
this.inherited(arguments);
|
|
this.$.generator.node = this.$.port.hasNode();
|
|
this.$.generator.generated = true;
|
|
this.reset();
|
|
},
|
|
resizeHandler: function() {
|
|
this.inherited(arguments);
|
|
this.refresh();
|
|
},
|
|
bottomUpChanged: function() {
|
|
this.$.generator.bottomUp = this.bottomUp;
|
|
this.$.page0.applyStyle(this.pageBound, null);
|
|
this.$.page1.applyStyle(this.pageBound, null);
|
|
this.pageBound = this.bottomUp ? "bottom" : "top";
|
|
if (this.hasNode()) {
|
|
this.reset();
|
|
}
|
|
},
|
|
multiSelectChanged: function() {
|
|
this.$.generator.setMultiSelect(this.multiSelect);
|
|
},
|
|
toggleSelectedChanged: function() {
|
|
this.$.generator.setToggleSelected(this.toggleSelected);
|
|
},
|
|
countChanged: function() {
|
|
if (this.hasNode()) {
|
|
this.updateMetrics();
|
|
}
|
|
},
|
|
updateMetrics: function() {
|
|
this.defaultPageHeight = this.rowsPerPage * (this.rowHeight || 100);
|
|
this.pageCount = Math.ceil(this.count / this.rowsPerPage);
|
|
this.portSize = 0;
|
|
for (var i=0; i < this.pageCount; i++) {
|
|
this.portSize += this.getPageHeight(i);
|
|
}
|
|
this.adjustPortSize();
|
|
},
|
|
generatePage: function(inPageNo, inTarget) {
|
|
this.page = inPageNo;
|
|
var r = this.$.generator.rowOffset = this.rowsPerPage * this.page;
|
|
var rpp = this.$.generator.count = Math.min(this.count - r, this.rowsPerPage);
|
|
var html = this.$.generator.generateChildHtml();
|
|
inTarget.setContent(html);
|
|
var pageHeight = inTarget.getBounds().height;
|
|
// if rowHeight is not set, use the height from the first generated page
|
|
if (!this.rowHeight && pageHeight > 0) {
|
|
this.rowHeight = Math.floor(pageHeight / rpp);
|
|
this.updateMetrics();
|
|
}
|
|
// update known page heights
|
|
if (!this.fixedHeight) {
|
|
var h0 = this.getPageHeight(inPageNo);
|
|
if (h0 != pageHeight && pageHeight > 0) {
|
|
this.pageHeights[inPageNo] = pageHeight;
|
|
this.portSize += pageHeight - h0;
|
|
}
|
|
}
|
|
},
|
|
update: function(inScrollTop) {
|
|
var updated = false;
|
|
// get page info for position
|
|
var pi = this.positionToPageInfo(inScrollTop);
|
|
// zone line position
|
|
var pos = pi.pos + this.scrollerHeight/2;
|
|
// leap-frog zone position
|
|
var k = Math.floor(pos/Math.max(pi.height, this.scrollerHeight) + 1/2) + pi.no;
|
|
// which page number for page0 (even number pages)?
|
|
var p = k % 2 == 0 ? k : k-1;
|
|
if (this.p0 != p && this.isPageInRange(p)) {
|
|
//this.log("update page0", p);
|
|
this.generatePage(p, this.$.page0);
|
|
this.positionPage(p, this.$.page0);
|
|
this.p0 = p;
|
|
updated = true;
|
|
}
|
|
// which page number for page1 (odd number pages)?
|
|
p = k % 2 == 0 ? Math.max(1, k-1) : k;
|
|
// position data page 1
|
|
if (this.p1 != p && this.isPageInRange(p)) {
|
|
//this.log("update page1", p);
|
|
this.generatePage(p, this.$.page1);
|
|
this.positionPage(p, this.$.page1);
|
|
this.p1 = p;
|
|
updated = true;
|
|
}
|
|
if (updated && !this.fixedHeight) {
|
|
this.adjustBottomPage();
|
|
this.adjustPortSize();
|
|
}
|
|
},
|
|
updateForPosition: function(inPos) {
|
|
this.update(this.calcPos(inPos));
|
|
},
|
|
calcPos: function(inPos) {
|
|
return (this.bottomUp ? (this.portSize - this.scrollerHeight - inPos) : inPos);
|
|
},
|
|
adjustBottomPage: function() {
|
|
var bp = this.p0 >= this.p1 ? this.$.page0 : this.$.page1;
|
|
this.positionPage(bp.pageNo, bp);
|
|
},
|
|
adjustPortSize: function() {
|
|
this.scrollerHeight = this.getBounds().height;
|
|
var s = Math.max(this.scrollerHeight, this.portSize);
|
|
this.$.port.applyStyle("height", s + "px");
|
|
},
|
|
positionPage: function(inPage, inTarget) {
|
|
inTarget.pageNo = inPage;
|
|
var y = this.pageToPosition(inPage);
|
|
inTarget.applyStyle(this.pageBound, y + "px");
|
|
},
|
|
pageToPosition: function(inPage) {
|
|
var y = 0;
|
|
var p = inPage;
|
|
while (p > 0) {
|
|
p--;
|
|
y += this.getPageHeight(p);
|
|
}
|
|
return y;
|
|
},
|
|
positionToPageInfo: function(inY) {
|
|
var page = -1;
|
|
var p = this.calcPos(inY);
|
|
var h = this.defaultPageHeight;
|
|
while (p >= 0) {
|
|
page++;
|
|
h = this.getPageHeight(page);
|
|
p -= h;
|
|
}
|
|
//page = Math.min(page, this.pageCount-1);
|
|
return {no: page, height: h, pos: p+h};
|
|
},
|
|
isPageInRange: function(inPage) {
|
|
return inPage == Math.max(0, Math.min(this.pageCount-1, inPage));
|
|
},
|
|
getPageHeight: function(inPageNo) {
|
|
return this.pageHeights[inPageNo] || this.defaultPageHeight;
|
|
},
|
|
invalidatePages: function() {
|
|
this.p0 = this.p1 = null;
|
|
// clear the html in our render targets
|
|
this.$.page0.setContent("");
|
|
this.$.page1.setContent("");
|
|
},
|
|
invalidateMetrics: function() {
|
|
this.pageHeights = [];
|
|
this.rowHeight = 0;
|
|
this.updateMetrics();
|
|
},
|
|
scroll: function(inSender, inEvent) {
|
|
var r = this.inherited(arguments);
|
|
this.update(this.getScrollTop());
|
|
return r;
|
|
},
|
|
//* @public
|
|
scrollToBottom: function() {
|
|
this.update(this.getScrollBounds().maxTop);
|
|
this.inherited(arguments);
|
|
},
|
|
setScrollTop: function(inScrollTop) {
|
|
this.update(inScrollTop);
|
|
this.inherited(arguments);
|
|
this.twiddle();
|
|
},
|
|
getScrollPosition: function() {
|
|
return this.calcPos(this.getScrollTop());
|
|
},
|
|
setScrollPosition: function(inPos) {
|
|
this.setScrollTop(this.calcPos(inPos));
|
|
},
|
|
//* Scrolls to a specific row.
|
|
scrollToRow: function(inRow) {
|
|
var page = Math.floor(inRow / this.rowsPerPage);
|
|
var pageRow = inRow % this.rowsPerPage;
|
|
var h = this.pageToPosition(page);
|
|
// update the page
|
|
this.updateForPosition(h);
|
|
// call pageToPosition again and this time should return the right pos since the page info is populated
|
|
h = this.pageToPosition(page);
|
|
this.setScrollPosition(h);
|
|
if (page == this.p0 || page == this.p1) {
|
|
var rowNode = this.$.generator.fetchRowNode(inRow);
|
|
if (rowNode) {
|
|
// calc row offset
|
|
var offset = rowNode.offsetTop;
|
|
if (this.bottomUp) {
|
|
offset = this.getPageHeight(page) - rowNode.offsetHeight - offset;
|
|
}
|
|
var y = this.getScrollPosition() + offset;
|
|
this.setScrollPosition(y);
|
|
}
|
|
}
|
|
},
|
|
//* Scrolls to the beginning of the list.
|
|
scrollToStart: function() {
|
|
this[this.bottomUp ? "scrollToBottom" : "scrollToTop"]();
|
|
},
|
|
//* Scrolls to the end of the list.
|
|
scrollToEnd: function() {
|
|
this[this.bottomUp ? "scrollToTop" : "scrollToBottom"]();
|
|
},
|
|
//* Re-renders the list at the current position.
|
|
refresh: function() {
|
|
this.invalidatePages();
|
|
this.update(this.getScrollTop());
|
|
this.stabilize();
|
|
|
|
//FIXME: Necessary evil for Android 4.0.4 refresh bug
|
|
if (enyo.platform.android === 4) {
|
|
this.twiddle();
|
|
}
|
|
},
|
|
//* Re-renders the list from the beginning.
|
|
reset: function() {
|
|
this.getSelection().clear();
|
|
this.invalidateMetrics();
|
|
this.invalidatePages();
|
|
this.stabilize();
|
|
this.scrollToStart();
|
|
},
|
|
/**
|
|
Returns the _selection_ component that manages the selection state for
|
|
this list.
|
|
*/
|
|
getSelection: function() {
|
|
return this.$.generator.getSelection();
|
|
},
|
|
//* Sets the selection state for the given row index.
|
|
select: function(inIndex, inData) {
|
|
return this.getSelection().select(inIndex, inData);
|
|
},
|
|
//* Gets the selection state for the given row index.
|
|
isSelected: function(inIndex) {
|
|
return this.$.generator.isSelected(inIndex);
|
|
},
|
|
/**
|
|
Re-renders the specified row. Call after making modifications to a row,
|
|
to force it to render.
|
|
*/
|
|
renderRow: function(inIndex) {
|
|
this.$.generator.renderRow(inIndex);
|
|
},
|
|
//* Prepares the row to become interactive.
|
|
prepareRow: function(inIndex) {
|
|
this.$.generator.prepareRow(inIndex);
|
|
},
|
|
//* Restores the row to being non-interactive.
|
|
lockRow: function() {
|
|
this.$.generator.lockRow();
|
|
},
|
|
/**
|
|
Performs a set of tasks by running the function _inFunc_ on a row (which
|
|
must be interactive at the time the tasks are performed). Locks the row
|
|
when done.
|
|
*/
|
|
performOnRow: function(inIndex, inFunc, inContext) {
|
|
this.$.generator.performOnRow(inIndex, inFunc, inContext);
|
|
},
|
|
//* @protected
|
|
animateFinish: function(inSender) {
|
|
this.twiddle();
|
|
return true;
|
|
},
|
|
// FIXME: Android 4.04 has issues with nested composited elements; for example, a SwipeableItem,
|
|
// can incorrectly generate taps on its content when it has slid off the screen;
|
|
// we address this BUG here by forcing the Scroller to "twiddle" which corrects the bug by
|
|
// provoking a dom update.
|
|
twiddle: function() {
|
|
var s = this.getStrategy();
|
|
enyo.call(s, "twiddle");
|
|
}
|
|
});
|