1302 lines
33 KiB
JavaScript
1302 lines
33 KiB
JavaScript
// Last commit: 1f0c355 (2013-09-18 11:01:11 -0400)
|
|
|
|
|
|
(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.x, position.y);
|
|
|
|
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() {
|
|
var el = document.createElement('div'), style = el.style;
|
|
|
|
var propPrefixes = ['Webkit', 'Moz', 'O', 'ms'];
|
|
|
|
function testProp(prop) {
|
|
if (prop in style) return prop;
|
|
var uppercaseProp = prop.charAt(0).toUpperCase() + prop.slice(1);
|
|
for (var i=0; i<propPrefixes.length; i++) {
|
|
var prefixedProp = propPrefixes[i] + uppercaseProp;
|
|
if (prefixedProp in style) {
|
|
return prefixedProp;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
var transformProp = testProp('transform');
|
|
var perspectiveProp = testProp('perspective');
|
|
|
|
var supports2D = transformProp !== null;
|
|
var supports3D = perspectiveProp !== null;
|
|
|
|
Ember.ListViewHelper = {
|
|
transformProp: transformProp,
|
|
applyTransform: (function(){
|
|
if (supports2D) {
|
|
return function(element, x, y){
|
|
element.style[transformProp] = 'translate(' + x + 'px, ' + y + 'px)';
|
|
};
|
|
} else {
|
|
return function(element, x, y){
|
|
element.style.top = y + 'px';
|
|
element.style.left = x + 'px';
|
|
};
|
|
}
|
|
})(),
|
|
apply3DTransform: (function(){
|
|
if (supports3D) {
|
|
return function(element, x, y){
|
|
element.style[transformProp] = 'translate3d(' + x + 'px, ' + y + 'px, 0)';
|
|
};
|
|
} else if (supports2D) {
|
|
return function(element, x, y){
|
|
element.style[transformProp] = 'translate(' + x + 'px, ' + y + 'px)';
|
|
};
|
|
} else {
|
|
return function(element, x, 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 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,
|
|
emptyViewClass: Ember.View,
|
|
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();
|
|
this.on('didInsertElement', syncListContainerWidth);
|
|
this.columnCountDidChange();
|
|
this._syncChildViews();
|
|
this._addContentArrayObserver();
|
|
},
|
|
|
|
_addContentArrayObserver: Ember.beforeObserver(function() {
|
|
addContentArrayObserver.call(this);
|
|
}, 'content'),
|
|
|
|
/**
|
|
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 = this._childViews;
|
|
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
|
|
@method positionOrderedChildViews
|
|
*/
|
|
positionOrderedChildViews: function() {
|
|
return this._childViews.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: Ember.ListViewHelper.applyTransform,
|
|
|
|
_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() {
|
|
var fieldRegex = /input|textarea|select/i,
|
|
hasTouch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch,
|
|
handleStart, handleMove, handleEnd, handleCancel,
|
|
startEvent, moveEvent, endEvent, cancelEvent;
|
|
if (hasTouch) {
|
|
startEvent = 'touchstart';
|
|
handleStart = function (e) {
|
|
var touch = e.touches[0],
|
|
target = touch && touch.target;
|
|
// avoid e.preventDefault() on fields
|
|
if (target && fieldRegex.test(target.tagName)) {
|
|
return;
|
|
}
|
|
bindWindow(this.scrollerEventHandlers);
|
|
this.willBeginScroll(e.touches, e.timeStamp);
|
|
e.preventDefault();
|
|
};
|
|
moveEvent = 'touchmove';
|
|
handleMove = function (e) {
|
|
this.continueScroll(e.touches, e.timeStamp);
|
|
};
|
|
endEvent = 'touchend';
|
|
handleEnd = function (e) {
|
|
// if we didn't end up scrolling we need to
|
|
// synthesize click since we did e.preventDefault()
|
|
// on touchstart
|
|
if (!this._isScrolling) {
|
|
synthesizeClick(e);
|
|
}
|
|
unbindWindow(this.scrollerEventHandlers);
|
|
this.endScroll(e.timeStamp);
|
|
};
|
|
cancelEvent = 'touchcancel';
|
|
handleCancel = function (e) {
|
|
unbindWindow(this.scrollerEventHandlers);
|
|
this.endScroll(e.timeStamp);
|
|
};
|
|
} else {
|
|
startEvent = 'mousedown';
|
|
handleStart = function (e) {
|
|
if (e.which !== 1) return;
|
|
var target = e.target;
|
|
// avoid e.preventDefault() on fields
|
|
if (target && fieldRegex.test(target.tagName)) {
|
|
return;
|
|
}
|
|
bindWindow(this.scrollerEventHandlers);
|
|
this.willBeginScroll([e], e.timeStamp);
|
|
e.preventDefault();
|
|
};
|
|
moveEvent = 'mousemove';
|
|
handleMove = function (e) {
|
|
this.continueScroll([e], e.timeStamp);
|
|
};
|
|
endEvent = 'mouseup';
|
|
handleEnd = function (e) {
|
|
unbindWindow(this.scrollerEventHandlers);
|
|
this.endScroll(e.timeStamp);
|
|
};
|
|
cancelEvent = 'mouseout';
|
|
handleCancel = function (e) {
|
|
if (e.relatedTarget) return;
|
|
unbindWindow(this.scrollerEventHandlers);
|
|
this.endScroll(e.timeStamp);
|
|
};
|
|
}
|
|
|
|
function handleWheel(e) {
|
|
this.mouseWheel(e);
|
|
e.preventDefault();
|
|
}
|
|
|
|
function bindElement(el, handlers) {
|
|
el.addEventListener(startEvent, handlers.start, false);
|
|
el.addEventListener('mousewheel', handlers.wheel, false);
|
|
}
|
|
|
|
function unbindElement(el, handlers) {
|
|
el.removeEventListener(startEvent, handlers.start, false);
|
|
el.removeEventListener('mousewheel', handlers.wheel, false);
|
|
}
|
|
|
|
function bindWindow(handlers) {
|
|
window.addEventListener(moveEvent, handlers.move, true);
|
|
window.addEventListener(endEvent, handlers.end, true);
|
|
window.addEventListener(cancelEvent, handlers.cancel, true);
|
|
}
|
|
|
|
function unbindWindow(handlers) {
|
|
window.removeEventListener(moveEvent, handlers.move, true);
|
|
window.removeEventListener(endEvent, handlers.end, true);
|
|
window.removeEventListener(cancelEvent, handlers.cancel, true);
|
|
}
|
|
|
|
Ember.VirtualListScrollerEvents = Ember.Mixin.create({
|
|
init: function() {
|
|
this.on('didInsertElement', this, 'bindScrollerEvents');
|
|
this.on('willDestroyElement', this, 'unbindScrollerEvents');
|
|
this.scrollerEventHandlers = {
|
|
start: bind(this, handleStart),
|
|
move: bind(this, handleMove),
|
|
end: bind(this, handleEnd),
|
|
cancel: bind(this, handleCancel),
|
|
wheel: bind(this, handleWheel)
|
|
};
|
|
return this._super();
|
|
},
|
|
bindScrollerEvents: function() {
|
|
var el = this.get('element'),
|
|
handlers = this.scrollerEventHandlers;
|
|
bindElement(el, handlers);
|
|
},
|
|
unbindScrollerEvents: function() {
|
|
var el = this.get('element'),
|
|
handlers = this.scrollerEventHandlers;
|
|
unbindElement(el, handlers);
|
|
unbindWindow(handlers);
|
|
}
|
|
});
|
|
|
|
function bind(view, handler) {
|
|
return function (evt) {
|
|
handler.call(view, evt);
|
|
};
|
|
}
|
|
|
|
function synthesizeClick(e) {
|
|
var point = e.changedTouches[0],
|
|
target = point.target,
|
|
ev;
|
|
if (target && fieldRegex.test(target.tagName)) {
|
|
ev = document.createEvent('MouseEvents');
|
|
ev.initMouseEvent('click', true, true, e.view, 1, point.screenX, point.screenY, point.clientX, point.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null);
|
|
return target.dispatchEvent(ev);
|
|
}
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
|
|
(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, Ember.VirtualListScrollerEvents, {
|
|
_isScrolling: false,
|
|
_mouseWheel: null,
|
|
css: {
|
|
position: 'relative',
|
|
overflow: 'hidden'
|
|
},
|
|
|
|
init: function(){
|
|
this._super();
|
|
this.setupScroller();
|
|
},
|
|
_scrollerTop: 0,
|
|
applyTransform: Ember.ListViewHelper.apply3DTransform,
|
|
|
|
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, 0, -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() {
|
|
this.listContainerElement = this.$('> .ember-list-container')[0];
|
|
},
|
|
|
|
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;
|
|
}
|
|
}
|
|
},
|
|
|
|
endScroll: function(timeStamp) {
|
|
this.scroller.doTouchEnd(timeStamp);
|
|
},
|
|
|
|
// 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;
|
|
}
|
|
});
|
|
|
|
})();
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
})();
|
|
|