1190 lines
30 KiB
JavaScript
1190 lines
30 KiB
JavaScript
|
(function() {
|
||
|
var get = Ember.get, set = Ember.set;
|
||
|
|
||
|
function samePosition(a, b) {
|
||
|
return a && b && a.x === b.x && a.y === b.y;
|
||
|
}
|
||
|
|
||
|
function positionElement() {
|
||
|
var element, position, _position;
|
||
|
|
||
|
Ember.instrument('view.updateContext.positionElement', this, function() {
|
||
|
element = get(this, 'element');
|
||
|
position = get(this, 'position');
|
||
|
_position = this._position;
|
||
|
|
||
|
if (!position || !element) { return; }
|
||
|
|
||
|
// TODO: avoid needing this by avoiding unnecessary
|
||
|
// calls to this method in the first place
|
||
|
if (samePosition(position, _position)) { return; }
|
||
|
this._parentView.applyTransform(element, position);
|
||
|
|
||
|
this._position = position;
|
||
|
}, this);
|
||
|
}
|
||
|
|
||
|
Ember.ListItemViewMixin = Ember.Mixin.create({
|
||
|
init: function(){
|
||
|
this._super();
|
||
|
this.one('didInsertElement', positionElement);
|
||
|
},
|
||
|
classNames: ['ember-list-item-view'],
|
||
|
_position: null,
|
||
|
_positionDidChange: Ember.observer(positionElement, 'position'),
|
||
|
_positionElement: positionElement
|
||
|
});
|
||
|
|
||
|
})();
|
||
|
|
||
|
|
||
|
|
||
|
(function() {
|
||
|
var get = Ember.get, set = Ember.set;
|
||
|
|
||
|
var backportedInnerString = function(buffer) {
|
||
|
var content = [], childBuffers = buffer.childBuffers;
|
||
|
|
||
|
Ember.ArrayPolyfills.forEach.call(childBuffers, function(buffer) {
|
||
|
var stringy = typeof buffer === 'string';
|
||
|
if (stringy) {
|
||
|
content.push(buffer);
|
||
|
} else {
|
||
|
buffer.array(content);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return content.join('');
|
||
|
};
|
||
|
|
||
|
function willInsertElementIfNeeded(view) {
|
||
|
if (view.willInsertElement) {
|
||
|
view.willInsertElement();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function didInsertElementIfNeeded(view) {
|
||
|
if (view.didInsertElement) {
|
||
|
view.didInsertElement();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function rerender() {
|
||
|
var element, buffer, context, hasChildViews;
|
||
|
element = get(this, 'element');
|
||
|
|
||
|
if (!element) { return; }
|
||
|
|
||
|
context = get(this, 'context');
|
||
|
|
||
|
// releases action helpers in contents
|
||
|
// this means though that the ListViewItem itself can't use classBindings or attributeBindings
|
||
|
// need support for rerender contents in ember
|
||
|
this.triggerRecursively('willClearRender');
|
||
|
|
||
|
if (this.lengthAfterRender > this.lengthBeforeRender) {
|
||
|
this.clearRenderedChildren();
|
||
|
this._childViews.length = this.lengthBeforeRender; // triage bug in ember
|
||
|
}
|
||
|
|
||
|
if (context) {
|
||
|
buffer = Ember.RenderBuffer();
|
||
|
buffer = this.renderToBuffer(buffer);
|
||
|
|
||
|
// check again for childViews, since rendering may have added some
|
||
|
hasChildViews = this._childViews.length > 0;
|
||
|
|
||
|
if (hasChildViews) {
|
||
|
this.invokeRecursively(willInsertElementIfNeeded, false);
|
||
|
}
|
||
|
|
||
|
element.innerHTML = buffer.innerString ? buffer.innerString() : backportedInnerString(buffer);
|
||
|
|
||
|
set(this, 'element', element);
|
||
|
|
||
|
this.transitionTo('inDOM');
|
||
|
|
||
|
if (hasChildViews) {
|
||
|
this.invokeRecursively(didInsertElementIfNeeded, false);
|
||
|
}
|
||
|
} else {
|
||
|
element.innerHTML = ''; // when there is no context, this view should be completely empty
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
The `Ember.ListViewItem` view class renders a
|
||
|
[div](https://developer.mozilla.org/en/HTML/Element/div) HTML element
|
||
|
with `ember-list-item-view` class. It allows you to specify a custom item
|
||
|
handlebars template for `Ember.ListView`.
|
||
|
|
||
|
Example:
|
||
|
|
||
|
```handlebars
|
||
|
<script type="text/x-handlebars" data-template-name="row_item">
|
||
|
{{name}}
|
||
|
</script>
|
||
|
```
|
||
|
|
||
|
```javascript
|
||
|
App.ListView = Ember.ListView.extend({
|
||
|
height: 500,
|
||
|
rowHeight: 20,
|
||
|
itemViewClass: Ember.ListItemView.extend({templateName: "row_item"})
|
||
|
});
|
||
|
```
|
||
|
|
||
|
@extends Ember.View
|
||
|
@class ListItemView
|
||
|
@namespace Ember
|
||
|
*/
|
||
|
Ember.ListItemView = Ember.View.extend(Ember.ListItemViewMixin, {
|
||
|
updateContext: function(newContext){
|
||
|
var context = get(this, 'context');
|
||
|
Ember.instrument('view.updateContext.render', this, function() {
|
||
|
if (context !== newContext) {
|
||
|
this.set('context', newContext);
|
||
|
if (newContext instanceof Ember.ObjectController) {
|
||
|
this.set('controller', newContext);
|
||
|
}
|
||
|
}
|
||
|
}, this);
|
||
|
},
|
||
|
rerender: function () { Ember.run.scheduleOnce('render', this, rerender); },
|
||
|
_contextDidChange: Ember.observer(rerender, 'context', 'controller')
|
||
|
});
|
||
|
|
||
|
})();
|
||
|
|
||
|
|
||
|
|
||
|
(function() {
|
||
|
var get = Ember.get, set = Ember.set;
|
||
|
|
||
|
Ember.ReusableListItemView = Ember.View.extend(Ember.ListItemViewMixin, {
|
||
|
init: function(){
|
||
|
this._super();
|
||
|
this.set('context', Ember.ObjectProxy.create());
|
||
|
},
|
||
|
isVisible: Ember.computed('context.content', function(){
|
||
|
return !!this.get('context.content');
|
||
|
}),
|
||
|
updateContext: function(newContext){
|
||
|
var context = get(this, 'context.content');
|
||
|
if (context !== newContext) {
|
||
|
if (this.state === 'inDOM') {
|
||
|
this.prepareForReuse(newContext);
|
||
|
}
|
||
|
set(this, 'context.content', newContext);
|
||
|
}
|
||
|
},
|
||
|
prepareForReuse: Ember.K
|
||
|
});
|
||
|
|
||
|
})();
|
||
|
|
||
|
|
||
|
|
||
|
(function() {
|
||
|
Ember.ListViewHelper = {
|
||
|
applyTransform: (function(){
|
||
|
var element = document.createElement('div');
|
||
|
|
||
|
if ('webkitTransform' in element.style){
|
||
|
return function(element, position){
|
||
|
var x = position.x,
|
||
|
y = position.y;
|
||
|
|
||
|
element.style.webkitTransform = 'translate3d(' + x + 'px, ' + y + 'px, 0)';
|
||
|
};
|
||
|
}else{
|
||
|
return function(element, position){
|
||
|
var x = position.x,
|
||
|
y = position.y;
|
||
|
|
||
|
element.style.top = y + 'px';
|
||
|
element.style.left = x + 'px';
|
||
|
};
|
||
|
}
|
||
|
})()
|
||
|
};
|
||
|
|
||
|
})();
|
||
|
|
||
|
|
||
|
|
||
|
(function() {
|
||
|
var get = Ember.get, set = Ember.set,
|
||
|
min = Math.min, max = Math.max, floor = Math.floor,
|
||
|
ceil = Math.ceil,
|
||
|
forEach = Ember.ArrayPolyfills.forEach;
|
||
|
|
||
|
function addContentArrayObserver() {
|
||
|
var content = get(this, 'content');
|
||
|
if (content) {
|
||
|
content.addArrayObserver(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function removeAndDestroy(object){
|
||
|
this.removeObject(object);
|
||
|
object.destroy();
|
||
|
}
|
||
|
|
||
|
function syncChildViews(){
|
||
|
Ember.run.once(this, '_syncChildViews');
|
||
|
}
|
||
|
|
||
|
function sortByContentIndex (viewOne, viewTwo){
|
||
|
return get(viewOne, 'contentIndex') - get(viewTwo, 'contentIndex');
|
||
|
}
|
||
|
|
||
|
function detectListItemViews(childView) {
|
||
|
return Ember.ListItemViewMixin.detect(childView);
|
||
|
}
|
||
|
|
||
|
function notifyMutationListeners() {
|
||
|
if (Ember.View.notifyMutationListeners) {
|
||
|
Ember.run.once(Ember.View, 'notifyMutationListeners');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var domManager = Ember.create(Ember.ContainerView.proto().domManager);
|
||
|
|
||
|
domManager.prepend = function(view, html) {
|
||
|
view.$('.ember-list-container').prepend(html);
|
||
|
notifyMutationListeners();
|
||
|
};
|
||
|
|
||
|
function syncListContainerWidth(){
|
||
|
var elementWidth, columnCount, containerWidth, element;
|
||
|
|
||
|
elementWidth = get(this, 'elementWidth');
|
||
|
columnCount = get(this, 'columnCount');
|
||
|
containerWidth = elementWidth * columnCount;
|
||
|
element = this.$('.ember-list-container');
|
||
|
|
||
|
if (containerWidth && element) {
|
||
|
element.css('width', containerWidth);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function enableProfilingOutput() {
|
||
|
function before(name, time, payload) {
|
||
|
console.time(name);
|
||
|
}
|
||
|
|
||
|
function after (name, time, payload) {
|
||
|
console.timeEnd(name);
|
||
|
}
|
||
|
|
||
|
if (Ember.ENABLE_PROFILING) {
|
||
|
Ember.subscribe('view._scrollContentTo', {
|
||
|
before: before,
|
||
|
after: after
|
||
|
});
|
||
|
Ember.subscribe('view.updateContext', {
|
||
|
before: before,
|
||
|
after: after
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
@class Ember.ListViewMixin
|
||
|
@namespace Ember
|
||
|
*/
|
||
|
Ember.ListViewMixin = Ember.Mixin.create({
|
||
|
itemViewClass: Ember.ListItemView,
|
||
|
classNames: ['ember-list-view'],
|
||
|
attributeBindings: ['style'],
|
||
|
domManager: domManager,
|
||
|
scrollTop: 0,
|
||
|
bottomPadding: 0,
|
||
|
_lastEndingIndex: 0,
|
||
|
paddingCount: 1,
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Setup a mixin.
|
||
|
- adding observer to content array
|
||
|
- creating child views based on height and length of the content array
|
||
|
|
||
|
@method init
|
||
|
*/
|
||
|
init: function() {
|
||
|
this._super();
|
||
|
enableProfilingOutput();
|
||
|
addContentArrayObserver.call(this);
|
||
|
this._syncChildViews();
|
||
|
this.columnCountDidChange();
|
||
|
this.on('didInsertElement', syncListContainerWidth);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
Called on your view when it should push strings of HTML into a
|
||
|
`Ember.RenderBuffer`.
|
||
|
|
||
|
Adds a [div](https://developer.mozilla.org/en-US/docs/HTML/Element/div)
|
||
|
with a required `ember-list-container` class.
|
||
|
|
||
|
@method render
|
||
|
@param {Ember.RenderBuffer} buffer The render buffer
|
||
|
*/
|
||
|
render: function(buffer) {
|
||
|
buffer.push('<div class="ember-list-container">');
|
||
|
this._super(buffer);
|
||
|
buffer.push('</div>');
|
||
|
},
|
||
|
|
||
|
willInsertElement: function() {
|
||
|
if (!this.get("height") || !this.get("rowHeight")) {
|
||
|
throw "A ListView must be created with a height and a rowHeight.";
|
||
|
}
|
||
|
this._super();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Sets inline styles of the view:
|
||
|
- height
|
||
|
- width
|
||
|
- position
|
||
|
- overflow
|
||
|
- -webkit-overflow
|
||
|
- overflow-scrolling
|
||
|
|
||
|
Called while attributes binding.
|
||
|
|
||
|
@property {Ember.ComputedProperty} style
|
||
|
*/
|
||
|
style: Ember.computed('height', 'width', function() {
|
||
|
var height, width, style, css;
|
||
|
|
||
|
height = get(this, 'height');
|
||
|
width = get(this, 'width');
|
||
|
css = get(this, 'css');
|
||
|
|
||
|
style = '';
|
||
|
|
||
|
if (height) { style += 'height:' + height + 'px;'; }
|
||
|
if (width) { style += 'width:' + width + 'px;'; }
|
||
|
|
||
|
for ( var rule in css ){
|
||
|
if (css.hasOwnProperty(rule)) {
|
||
|
style += rule + ':' + css[rule] + ';';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return style;
|
||
|
}),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Performs visual scrolling. Is overridden in Ember.ListView.
|
||
|
|
||
|
@method scrollTo
|
||
|
*/
|
||
|
scrollTo: function(y) {
|
||
|
throw 'must override to perform the visual scroll and effectively delegate to _scrollContentTo';
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Internal method used to force scroll position
|
||
|
|
||
|
@method scrollTo
|
||
|
*/
|
||
|
_scrollTo: Ember.K,
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@method _scrollContentTo
|
||
|
*/
|
||
|
_scrollContentTo: function(y) {
|
||
|
var startingIndex, endingIndex,
|
||
|
contentIndex, visibleEndingIndex, maxContentIndex,
|
||
|
contentIndexEnd, contentLength, scrollTop;
|
||
|
|
||
|
scrollTop = max(0, y);
|
||
|
|
||
|
Ember.instrument('view._scrollContentTo', {
|
||
|
scrollTop: scrollTop,
|
||
|
content: get(this, 'content'),
|
||
|
startingIndex: this._startingIndex(),
|
||
|
endingIndex: min(max(get(this, 'content.length') - 1, 0), this._startingIndex() + this._numChildViewsForViewport())
|
||
|
}, function () {
|
||
|
contentLength = get(this, 'content.length');
|
||
|
set(this, 'scrollTop', scrollTop);
|
||
|
|
||
|
maxContentIndex = max(contentLength - 1, 0);
|
||
|
|
||
|
startingIndex = this._startingIndex();
|
||
|
visibleEndingIndex = startingIndex + this._numChildViewsForViewport();
|
||
|
|
||
|
endingIndex = min(maxContentIndex, visibleEndingIndex);
|
||
|
|
||
|
this.trigger('scrollYChanged', y);
|
||
|
|
||
|
if (startingIndex === this._lastStartingIndex &&
|
||
|
endingIndex === this._lastEndingIndex) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._reuseChildren();
|
||
|
|
||
|
this._lastStartingIndex = startingIndex;
|
||
|
this._lastEndingIndex = endingIndex;
|
||
|
}, this);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Computes the height for a `Ember.ListView` scrollable container div.
|
||
|
You must specify `rowHeight` parameter for the height to be computed properly.
|
||
|
|
||
|
@property {Ember.ComputedProperty} totalHeight
|
||
|
*/
|
||
|
totalHeight: Ember.computed('content.length', 'rowHeight', 'columnCount', 'bottomPadding', function() {
|
||
|
var contentLength, rowHeight, columnCount, bottomPadding;
|
||
|
|
||
|
contentLength = get(this, 'content.length');
|
||
|
rowHeight = get(this, 'rowHeight');
|
||
|
columnCount = get(this, 'columnCount');
|
||
|
bottomPadding = get(this, 'bottomPadding');
|
||
|
|
||
|
return ((ceil(contentLength / columnCount)) * rowHeight) + bottomPadding;
|
||
|
}),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@method _prepareChildForReuse
|
||
|
*/
|
||
|
_prepareChildForReuse: function(childView) {
|
||
|
childView.prepareForReuse();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@method _reuseChildForContentIndex
|
||
|
*/
|
||
|
_reuseChildForContentIndex: function(childView, contentIndex) {
|
||
|
var content, context, newContext, childsCurrentContentIndex, position, enableProfiling;
|
||
|
|
||
|
content = get(this, 'content');
|
||
|
enableProfiling = get(this, 'enableProfiling');
|
||
|
position = this.positionForIndex(contentIndex);
|
||
|
set(childView, 'position', position);
|
||
|
|
||
|
set(childView, 'contentIndex', contentIndex);
|
||
|
|
||
|
if (enableProfiling) {
|
||
|
Ember.instrument('view._reuseChildForContentIndex', position, function(){}, this);
|
||
|
}
|
||
|
|
||
|
newContext = content.objectAt(contentIndex);
|
||
|
childView.updateContext(newContext);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@method positionForIndex
|
||
|
*/
|
||
|
positionForIndex: function(index){
|
||
|
var elementWidth, width, columnCount, rowHeight, y, x;
|
||
|
|
||
|
elementWidth = get(this, 'elementWidth') || 1;
|
||
|
width = get(this, 'width') || 1;
|
||
|
columnCount = get(this, 'columnCount');
|
||
|
rowHeight = get(this, 'rowHeight');
|
||
|
|
||
|
y = (rowHeight * floor(index/columnCount));
|
||
|
x = (index % columnCount) * elementWidth;
|
||
|
|
||
|
return {
|
||
|
y: y,
|
||
|
x: x
|
||
|
};
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@method _childViewCount
|
||
|
*/
|
||
|
_childViewCount: function() {
|
||
|
var contentLength, childViewCountForHeight;
|
||
|
|
||
|
contentLength = get(this, 'content.length');
|
||
|
childViewCountForHeight = this._numChildViewsForViewport();
|
||
|
|
||
|
return min(contentLength, childViewCountForHeight);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Returns a number of columns in the Ember.ListView (for grid layout).
|
||
|
|
||
|
If you want to have a multi column layout, you need to specify both
|
||
|
`width` and `elementWidth`.
|
||
|
|
||
|
If no `elementWidth` is specified, it returns `1`. Otherwise, it will
|
||
|
try to fit as many columns as possible for a given `width`.
|
||
|
|
||
|
@property {Ember.ComputedProperty} columnCount
|
||
|
*/
|
||
|
columnCount: Ember.computed('width', 'elementWidth', function() {
|
||
|
var elementWidth, width, count;
|
||
|
|
||
|
elementWidth = get(this, 'elementWidth');
|
||
|
width = get(this, 'width');
|
||
|
|
||
|
if (elementWidth) {
|
||
|
count = floor(width / elementWidth);
|
||
|
} else {
|
||
|
count = 1;
|
||
|
}
|
||
|
|
||
|
return count;
|
||
|
}),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Fires every time column count is changed.
|
||
|
|
||
|
@event columnCountDidChange
|
||
|
*/
|
||
|
columnCountDidChange: Ember.observer(function(){
|
||
|
var ratio, currentScrollTop, proposedScrollTop, maxScrollTop,
|
||
|
scrollTop, lastColumnCount, newColumnCount, element;
|
||
|
|
||
|
lastColumnCount = this._lastColumnCount;
|
||
|
|
||
|
currentScrollTop = get(this, 'scrollTop');
|
||
|
newColumnCount = get(this, 'columnCount');
|
||
|
maxScrollTop = get(this, 'maxScrollTop');
|
||
|
element = get(this, 'element');
|
||
|
|
||
|
this._lastColumnCount = newColumnCount;
|
||
|
|
||
|
if (lastColumnCount) {
|
||
|
ratio = (lastColumnCount / newColumnCount);
|
||
|
proposedScrollTop = currentScrollTop * ratio;
|
||
|
scrollTop = min(maxScrollTop, proposedScrollTop);
|
||
|
|
||
|
this._scrollTo(scrollTop);
|
||
|
set(this, 'scrollTop', scrollTop);
|
||
|
}
|
||
|
|
||
|
if (arguments.length > 0) {
|
||
|
// invoked by observer
|
||
|
Ember.run.schedule('afterRender', this, syncListContainerWidth);
|
||
|
}
|
||
|
}, 'columnCount'),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Computes max possible scrollTop value given the visible viewport
|
||
|
and scrollable container div height.
|
||
|
|
||
|
@property {Ember.ComputedProperty} maxScrollTop
|
||
|
*/
|
||
|
maxScrollTop: Ember.computed('height', 'totalHeight', function(){
|
||
|
var totalHeight, viewportHeight;
|
||
|
|
||
|
totalHeight = get(this, 'totalHeight'),
|
||
|
viewportHeight = get(this, 'height');
|
||
|
|
||
|
return max(0, totalHeight - viewportHeight);
|
||
|
}),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Computes the number of views that would fit in the viewport area.
|
||
|
You must specify `height` and `rowHeight` parameters for the number of
|
||
|
views to be computed properly.
|
||
|
|
||
|
@method _numChildViewsForViewport
|
||
|
*/
|
||
|
_numChildViewsForViewport: function() {
|
||
|
var height, rowHeight, paddingCount, columnCount;
|
||
|
|
||
|
height = get(this, 'height');
|
||
|
rowHeight = get(this, 'rowHeight');
|
||
|
paddingCount = get(this, 'paddingCount');
|
||
|
columnCount = get(this, 'columnCount');
|
||
|
|
||
|
return (ceil(height / rowHeight) * columnCount) + (paddingCount * columnCount);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Computes the starting index of the item views array.
|
||
|
Takes `scrollTop` property of the element into account.
|
||
|
|
||
|
Is used in `_syncChildViews`.
|
||
|
|
||
|
@method _startingIndex
|
||
|
*/
|
||
|
_startingIndex: function() {
|
||
|
var scrollTop, rowHeight, columnCount, calculatedStartingIndex,
|
||
|
contentLength, largestStartingIndex;
|
||
|
|
||
|
contentLength = get(this, 'content.length');
|
||
|
scrollTop = get(this, 'scrollTop');
|
||
|
rowHeight = get(this, 'rowHeight');
|
||
|
columnCount = get(this, 'columnCount');
|
||
|
|
||
|
calculatedStartingIndex = floor(scrollTop / rowHeight) * columnCount;
|
||
|
|
||
|
largestStartingIndex = max(contentLength - 1, 0);
|
||
|
|
||
|
return min(calculatedStartingIndex, largestStartingIndex);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@event contentWillChange
|
||
|
*/
|
||
|
contentWillChange: Ember.beforeObserver(function() {
|
||
|
var content;
|
||
|
|
||
|
content = get(this, 'content');
|
||
|
|
||
|
if (content) {
|
||
|
content.removeArrayObserver(this);
|
||
|
}
|
||
|
}, 'content'),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@event contentDidChange
|
||
|
*/
|
||
|
contentDidChange: Ember.observer(function() {
|
||
|
addContentArrayObserver.call(this);
|
||
|
syncChildViews.call(this);
|
||
|
}, 'content'),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@property {Function} needsSyncChildViews
|
||
|
*/
|
||
|
needsSyncChildViews: Ember.observer(syncChildViews, 'height', 'width', 'columnCount'),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Returns a new item view. Takes `contentIndex` to set the context
|
||
|
of the returned view properly.
|
||
|
|
||
|
@param {Number} contentIndex item index in the content array
|
||
|
@method _addItemView
|
||
|
*/
|
||
|
_addItemView: function(contentIndex){
|
||
|
var itemViewClass, childView;
|
||
|
|
||
|
itemViewClass = get(this, 'itemViewClass');
|
||
|
childView = this.createChildView(itemViewClass);
|
||
|
|
||
|
this.pushObject(childView);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Intelligently manages the number of childviews.
|
||
|
|
||
|
@method _syncChildViews
|
||
|
**/
|
||
|
_syncChildViews: function(){
|
||
|
var itemViewClass, startingIndex, childViewCount,
|
||
|
endingIndex, numberOfChildViews, numberOfChildViewsNeeded,
|
||
|
childViews, count, delta, index, childViewsLength, contentIndex;
|
||
|
|
||
|
if (get(this, 'isDestroyed') || get(this, 'isDestroying')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
childViewCount = this._childViewCount();
|
||
|
childViews = this.positionOrderedChildViews();
|
||
|
|
||
|
startingIndex = this._startingIndex();
|
||
|
endingIndex = startingIndex + childViewCount;
|
||
|
|
||
|
numberOfChildViewsNeeded = childViewCount;
|
||
|
numberOfChildViews = childViews.length;
|
||
|
|
||
|
delta = numberOfChildViewsNeeded - numberOfChildViews;
|
||
|
|
||
|
if (delta === 0) {
|
||
|
// no change
|
||
|
} else if (delta > 0) {
|
||
|
// more views are needed
|
||
|
contentIndex = this._lastEndingIndex;
|
||
|
|
||
|
for (count = 0; count < delta; count++, contentIndex++) {
|
||
|
this._addItemView(contentIndex);
|
||
|
}
|
||
|
|
||
|
} else {
|
||
|
// less views are needed
|
||
|
forEach.call(
|
||
|
childViews.splice(numberOfChildViewsNeeded, numberOfChildViews),
|
||
|
removeAndDestroy,
|
||
|
this
|
||
|
);
|
||
|
}
|
||
|
|
||
|
this._scrollContentTo(get(this, 'scrollTop'));
|
||
|
|
||
|
// if _scrollContentTo short-circuits, we still need
|
||
|
// to call _reuseChildren to get new views positioned
|
||
|
// and rendered correctly
|
||
|
this._reuseChildren();
|
||
|
|
||
|
this._lastStartingIndex = startingIndex;
|
||
|
this._lastEndingIndex = this._lastEndingIndex + delta;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@method _reuseChildren
|
||
|
*/
|
||
|
_reuseChildren: function(){
|
||
|
var contentLength, childViews, childViewsLength,
|
||
|
startingIndex, endingIndex, childView, attrs,
|
||
|
contentIndex, visibleEndingIndex, maxContentIndex,
|
||
|
contentIndexEnd, scrollTop;
|
||
|
|
||
|
scrollTop = get(this, 'scrollTop');
|
||
|
contentLength = get(this, 'content.length');
|
||
|
maxContentIndex = max(contentLength - 1, 0);
|
||
|
childViews = get(this, 'listItemViews');
|
||
|
childViewsLength = childViews.length;
|
||
|
|
||
|
startingIndex = this._startingIndex();
|
||
|
visibleEndingIndex = startingIndex + this._numChildViewsForViewport();
|
||
|
|
||
|
endingIndex = min(maxContentIndex, visibleEndingIndex);
|
||
|
|
||
|
this.trigger('scrollContentTo', scrollTop);
|
||
|
|
||
|
contentIndexEnd = min(visibleEndingIndex, startingIndex + childViewsLength);
|
||
|
|
||
|
for (contentIndex = startingIndex; contentIndex < contentIndexEnd; contentIndex++) {
|
||
|
childView = childViews[contentIndex % childViewsLength];
|
||
|
this._reuseChildForContentIndex(childView, contentIndex);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
|
||
|
Returns an array of current ListItemView views in the visible area
|
||
|
when you start to scroll.
|
||
|
|
||
|
@property {Ember.ComputedProperty} listItemViews
|
||
|
*/
|
||
|
listItemViews: Ember.computed('[]', function(){
|
||
|
return this.filter(detectListItemViews);
|
||
|
}),
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@method positionOrderedChildViews
|
||
|
*/
|
||
|
positionOrderedChildViews: function() {
|
||
|
return get(this, 'listItemViews').sort(sortByContentIndex);
|
||
|
},
|
||
|
|
||
|
arrayWillChange: Ember.K,
|
||
|
|
||
|
/**
|
||
|
@private
|
||
|
@event arrayDidChange
|
||
|
*/
|
||
|
// TODO: refactor
|
||
|
arrayDidChange: function(content, start, removedCount, addedCount) {
|
||
|
var index, contentIndex;
|
||
|
|
||
|
if (this.state === 'inDOM') {
|
||
|
// ignore if all changes are out of the visible change
|
||
|
if( start >= this._lastStartingIndex || start < this._lastEndingIndex) {
|
||
|
index = 0;
|
||
|
// ignore all changes not in the visible range
|
||
|
// this can re-position many, rather then causing a cascade of re-renders
|
||
|
forEach.call(
|
||
|
this.positionOrderedChildViews(),
|
||
|
function(childView) {
|
||
|
contentIndex = this._lastStartingIndex + index;
|
||
|
this._reuseChildForContentIndex(childView, contentIndex);
|
||
|
index++;
|
||
|
},
|
||
|
this
|
||
|
);
|
||
|
}
|
||
|
|
||
|
syncChildViews.call(this);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
})();
|
||
|
|
||
|
|
||
|
|
||
|
(function() {
|
||
|
var get = Ember.get, set = Ember.set;
|
||
|
|
||
|
/**
|
||
|
The `Ember.ListView` view class renders a
|
||
|
[div](https://developer.mozilla.org/en/HTML/Element/div) HTML element,
|
||
|
with `ember-list-view` class.
|
||
|
|
||
|
The context of each item element within the `Ember.ListView` are populated
|
||
|
from the objects in the `Element.ListView`'s `content` property.
|
||
|
|
||
|
### `content` as an Array of Objects
|
||
|
|
||
|
The simplest version of an `Ember.ListView` takes an array of object as its
|
||
|
`content` property. The object will be used as the `context` each item element
|
||
|
inside the rendered `div`.
|
||
|
|
||
|
Example:
|
||
|
|
||
|
```javascript
|
||
|
App.contributors = [{ name: 'Stefan Penner' }, { name: 'Alex Navasardyan' }, { name: 'Rey Cohen'}];
|
||
|
```
|
||
|
|
||
|
```handlebars
|
||
|
{{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50}}
|
||
|
{{name}}
|
||
|
{{/collection}}
|
||
|
```
|
||
|
|
||
|
Would result in the following HTML:
|
||
|
|
||
|
```html
|
||
|
<div id="ember181" class="ember-view ember-list-view" style="height:500px;width:500px;position:relative;overflow:scroll;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;">
|
||
|
<div class="ember-list-container">
|
||
|
<div id="ember186" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 0px, 0);">
|
||
|
<script id="metamorph-0-start" type="text/x-placeholder"></script>Stefan Penner<script id="metamorph-0-end" type="text/x-placeholder"></script>
|
||
|
</div>
|
||
|
<div id="ember187" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 50px, 0);">
|
||
|
<script id="metamorph-1-start" type="text/x-placeholder"></script>Alex Navasardyan<script id="metamorph-1-end" type="text/x-placeholder"></script>
|
||
|
</div>
|
||
|
<div id="ember188" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 100px, 0);">
|
||
|
<script id="metamorph-2-start" type="text/x-placeholder"></script>Rey Cohen<script id="metamorph-2-end" type="text/x-placeholder"></script>
|
||
|
</div>
|
||
|
<div id="ember189" class="ember-view ember-list-scrolling-view" style="height: 150px"></div>
|
||
|
</div>
|
||
|
</div>
|
||
|
```
|
||
|
|
||
|
By default `Ember.ListView` provides support for `height`,
|
||
|
`rowHeight`, `width`, `elementWidth`, `scrollTop` parameters.
|
||
|
|
||
|
Note, that `height` and `rowHeight` are required parameters.
|
||
|
|
||
|
```handlebars
|
||
|
{{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50}}
|
||
|
{{name}}
|
||
|
{{/collection}}
|
||
|
```
|
||
|
|
||
|
If you would like to have multiple columns in your view layout, you can
|
||
|
set `width` and `elementWidth` parameters respectively.
|
||
|
|
||
|
```handlebars
|
||
|
{{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50 width=500 elementWidth=80}}
|
||
|
{{name}}
|
||
|
{{/collection}}
|
||
|
```
|
||
|
|
||
|
### extending `Ember.ListView`
|
||
|
|
||
|
Example:
|
||
|
|
||
|
```handlebars
|
||
|
{{view App.ListView contentBinding="content"}}
|
||
|
|
||
|
<script type="text/x-handlebars" data-template-name="row_item">
|
||
|
{{name}}
|
||
|
</script>
|
||
|
```
|
||
|
|
||
|
```javascript
|
||
|
App.ListView = Ember.ListView.extend({
|
||
|
height: 500,
|
||
|
width: 500,
|
||
|
elementWidth: 80,
|
||
|
rowHeight: 20,
|
||
|
itemViewClass: Ember.ListItemView.extend({templateName: "row_item"})
|
||
|
});
|
||
|
```
|
||
|
|
||
|
@extends Ember.ContainerView
|
||
|
@class ListView
|
||
|
@namespace Ember
|
||
|
*/
|
||
|
Ember.ListView = Ember.ContainerView.extend(Ember.ListViewMixin, {
|
||
|
css: {
|
||
|
position: 'relative',
|
||
|
overflow: 'scroll',
|
||
|
'-webkit-overflow-scrolling': 'touch',
|
||
|
'overflow-scrolling': 'touch'
|
||
|
},
|
||
|
|
||
|
applyTransform: function(element, position){
|
||
|
var x = position.x,
|
||
|
y = position.y;
|
||
|
|
||
|
element.style.top = y + 'px';
|
||
|
element.style.left = x + 'px';
|
||
|
},
|
||
|
|
||
|
_scrollTo: function(scrollTop) {
|
||
|
var element = get(this, 'element');
|
||
|
|
||
|
if (element) { element.scrollTop = scrollTop; }
|
||
|
},
|
||
|
|
||
|
didInsertElement: function() {
|
||
|
var that, element;
|
||
|
|
||
|
that = this,
|
||
|
element = get(this, 'element');
|
||
|
|
||
|
this._updateScrollableHeight();
|
||
|
|
||
|
this._scroll = function(e) { that.scroll(e); };
|
||
|
|
||
|
Ember.$(element).on('scroll', this._scroll);
|
||
|
},
|
||
|
|
||
|
willDestroyElement: function() {
|
||
|
var element;
|
||
|
|
||
|
element = get(this, 'element');
|
||
|
|
||
|
Ember.$(element).off('scroll', this._scroll);
|
||
|
},
|
||
|
|
||
|
scroll: function(e) {
|
||
|
Ember.run(this, this.scrollTo, e.target.scrollTop);
|
||
|
},
|
||
|
|
||
|
scrollTo: function(y){
|
||
|
var element = get(this, 'element');
|
||
|
this._scrollTo(y);
|
||
|
this._scrollContentTo(y);
|
||
|
},
|
||
|
|
||
|
totalHeightDidChange: Ember.observer(function () {
|
||
|
Ember.run.scheduleOnce('afterRender', this, this._updateScrollableHeight);
|
||
|
}, 'totalHeight'),
|
||
|
|
||
|
_updateScrollableHeight: function () {
|
||
|
if (this.state === 'inDOM') {
|
||
|
this.$('.ember-list-container').css({
|
||
|
height: get(this, 'totalHeight')
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
})();
|
||
|
|
||
|
|
||
|
|
||
|
(function() {
|
||
|
/*global Scroller*/
|
||
|
var max = Math.max, get = Ember.get, set = Ember.set;
|
||
|
|
||
|
function updateScrollerDimensions(target) {
|
||
|
var width, height, totalHeight;
|
||
|
|
||
|
target = target || this;
|
||
|
|
||
|
width = get(target, 'width');
|
||
|
height = get(target, 'height');
|
||
|
totalHeight = get(target, 'totalHeight');
|
||
|
|
||
|
target.scroller.setDimensions(width, height, width, totalHeight);
|
||
|
target.trigger('scrollerDimensionsDidChange');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
VirtualListView
|
||
|
|
||
|
@class VirtualListView
|
||
|
@namespace Ember
|
||
|
*/
|
||
|
Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, {
|
||
|
_isScrolling: false,
|
||
|
css: {
|
||
|
position: 'relative',
|
||
|
overflow: 'hidden'
|
||
|
},
|
||
|
|
||
|
init: function(){
|
||
|
this._super();
|
||
|
this.setupScroller();
|
||
|
},
|
||
|
_scrollerTop: 0,
|
||
|
applyTransform: Ember.ListViewHelper.applyTransform,
|
||
|
|
||
|
setupScroller: function(){
|
||
|
var view, y;
|
||
|
|
||
|
view = this;
|
||
|
|
||
|
view.scroller = new Scroller(function(left, top, zoom) {
|
||
|
if (view.state !== 'inDOM') { return; }
|
||
|
|
||
|
if (view.listContainerElement) {
|
||
|
view.applyTransform(view.listContainerElement, {x: 0, y: -top});
|
||
|
view._scrollerTop = top;
|
||
|
view._scrollContentTo(top);
|
||
|
}
|
||
|
}, {
|
||
|
scrollingX: false,
|
||
|
scrollingComplete: function(){
|
||
|
view.trigger('scrollingDidComplete');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
view.trigger('didInitializeScroller');
|
||
|
updateScrollerDimensions(view);
|
||
|
},
|
||
|
|
||
|
scrollerDimensionsNeedToChange: Ember.observer(function() {
|
||
|
Ember.run.once(this, updateScrollerDimensions);
|
||
|
}, 'width', 'height', 'totalHeight'),
|
||
|
|
||
|
didInsertElement: function() {
|
||
|
var that, listContainerElement;
|
||
|
|
||
|
that = this;
|
||
|
this.listContainerElement = this.$('> .ember-list-container')[0];
|
||
|
|
||
|
this._mouseWheel = function(e) { that.mouseWheel(e); };
|
||
|
this.$().on('mousewheel', this._mouseWheel);
|
||
|
},
|
||
|
|
||
|
willDestroyElement: function() {
|
||
|
this.$().off('mousewheel', this._mouseWheel);
|
||
|
},
|
||
|
|
||
|
willBeginScroll: function(touches, timeStamp) {
|
||
|
this._isScrolling = false;
|
||
|
this.trigger('scrollingDidStart');
|
||
|
|
||
|
this.scroller.doTouchStart(touches, timeStamp);
|
||
|
},
|
||
|
|
||
|
continueScroll: function(touches, timeStamp) {
|
||
|
var startingScrollTop, endingScrollTop, event;
|
||
|
|
||
|
if (this._isScrolling) {
|
||
|
this.scroller.doTouchMove(touches, timeStamp);
|
||
|
} else {
|
||
|
startingScrollTop = this._scrollerTop;
|
||
|
|
||
|
this.scroller.doTouchMove(touches, timeStamp);
|
||
|
|
||
|
endingScrollTop = this._scrollerTop;
|
||
|
|
||
|
if (startingScrollTop !== endingScrollTop) {
|
||
|
event = Ember.$.Event("scrollerstart");
|
||
|
Ember.$(touches[0].target).trigger(event);
|
||
|
|
||
|
this._isScrolling = true;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// api
|
||
|
scrollTo: function(y, animate) {
|
||
|
if (animate === undefined) {
|
||
|
animate = true;
|
||
|
}
|
||
|
|
||
|
this.scroller.scrollTo(0, y, animate, 1);
|
||
|
},
|
||
|
|
||
|
// events
|
||
|
mouseWheel: function(e){
|
||
|
var inverted, delta, candidatePosition;
|
||
|
|
||
|
inverted = e.webkitDirectionInvertedFromDevice;
|
||
|
delta = e.wheelDeltaY * (inverted ? 0.8 : -0.8);
|
||
|
candidatePosition = this.scroller.__scrollTop + delta;
|
||
|
|
||
|
if ((candidatePosition >= 0) && (candidatePosition <= this.scroller.__maxScrollTop)) {
|
||
|
this.scroller.scrollBy(0, delta, true);
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
endScroll: function(timeStamp) {
|
||
|
this.scroller.doTouchEnd(timeStamp);
|
||
|
},
|
||
|
|
||
|
touchStart: function(e){
|
||
|
e = e.originalEvent || e;
|
||
|
this.willBeginScroll(e.touches, e.timeStamp);
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
touchMove: function(e){
|
||
|
e = e.originalEvent || e;
|
||
|
this.continueScroll(e.touches, e.timeStamp);
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
touchEnd: function(e){
|
||
|
e = e.originalEvent || e;
|
||
|
this.endScroll(e.timeStamp);
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
mouseDown: function(e){
|
||
|
this.willBeginScroll([e], e.timeStamp);
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
mouseMove: function(e){
|
||
|
this.continueScroll([e], e.timeStamp);
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
mouseUp: function(e){
|
||
|
this.endScroll(e.timeStamp);
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
mouseLeave: function(e){
|
||
|
this.endScroll(e.timeStamp);
|
||
|
return false;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
})();
|
||
|
|
||
|
|
||
|
|
||
|
(function() {
|
||
|
|
||
|
})();
|