From 816de0918c28461cc2d1e3457348fd5b6e11950f Mon Sep 17 00:00:00 2001 From: Lionel LASKE Date: Tue, 18 Sep 2012 19:16:20 +0000 Subject: Initial version --- (limited to 'html/lib/layout/list/source/List.js') diff --git a/html/lib/layout/list/source/List.js b/html/lib/layout/list/source/List.js new file mode 100644 index 0000000..c4252b9 --- /dev/null +++ b/html/lib/layout/list/source/List.js @@ -0,0 +1,368 @@ +/** + 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 enyo.Control and + enyo.Image. + + 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: }_ + */ + 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"); + } +}); -- cgit v0.9.1