(function () { /** Display a list of cloaked items @class CloakedContainerView @extends Ember.View @namespace Ember **/ Ember.CloakedCollectionView = Ember.CollectionView.extend({ topVisible: null, bottomVisible: null, init: function() { var cloakView = this.get('cloakView'), idProperty = this.get('idProperty') || 'id'; // Set the slack ratio differently to allow for more or less slack in preloading var slackRatio = parseFloat(this.get('slackRatio')); if (!slackRatio) { this.set('slackRatio', 1.0); } this.set('itemViewClass', Ember.CloakedView.extend({ classNames: [cloakView + '-cloak'], cloaks: cloakView, defaultHeight: this.get('defaultHeight') || 100, init: function() { this._super(); this.set('elementId', cloakView + '-cloak-' + this.get('content.' + idProperty)); } })); this._super(); Ember.run.next(this, 'scrolled'); }, /** If the topmost visible view changed, we will notify the controller if it has an appropriate hook. @method _topVisibleChanged @observes topVisible **/ _topVisibleChanged: function() { var controller = this.get('controller'); if (controller.topVisibleChanged) { controller.topVisibleChanged(this.get('topVisible')); } }.observes('topVisible'), /** If the bottommost visible view changed, we will notify the controller if it has an appropriate hook. @method _bottomVisible @observes bottomVisible **/ _bottomVisible: function() { var controller = this.get('controller'); if (controller.bottomVisibleChanged) { controller.bottomVisibleChanged(this.get('bottomVisible')); } }.observes('bottomVisible'), /** Binary search for finding the topmost view on screen. @method findTopView @param {Array} childViews the childViews to search through @param {Number} windowTop The top of the viewport to search against @param {Number} min The minimum index to search through of the child views @param {Number} max The max index to search through of the child views @returns {Number} the index into childViews of the topmost view **/ findTopView: function(childViews, viewportTop, min, max) { if (max < min) { return min; } var mid = Math.floor((min + max) / 2), $view = childViews[mid].$(), viewBottom = $view.offset().top + $view.height(); if (viewBottom > viewportTop) { return this.findTopView(childViews, viewportTop, min, mid-1); } else { return this.findTopView(childViews, viewportTop, mid+1, max); } }, /** Determine what views are onscreen and cloak/uncloak them as necessary. @method scrolled **/ scrolled: function() { var childViews = this.get('childViews'); if ((!childViews) || (childViews.length === 0)) { return; } var toUncloak = [], $w = $(window), windowHeight = window.innerHeight ? window.innerHeight : $w.height(), windowTop = $w.scrollTop(), slack = Math.round(windowHeight * this.get('slackRatio')), viewportTop = windowTop - slack, windowBottom = windowTop + windowHeight, viewportBottom = windowBottom + slack, topView = this.findTopView(childViews, viewportTop, 0, childViews.length-1), bodyHeight = $('body').height(), bottomView = topView, onscreen = []; if (windowBottom > bodyHeight) { windowBottom = bodyHeight; } if (viewportBottom > bodyHeight) { viewportBottom = bodyHeight; } // Find the bottom view and what's onscreen while (bottomView < childViews.length) { var view = childViews[bottomView], $view = view.$(), viewTop = $view.offset().top, viewBottom = viewTop + $view.height(); if (viewTop > viewportBottom) { break; } toUncloak.push(view); if (viewBottom > windowTop && viewTop <= windowBottom) { onscreen.push(view.get('content')); } bottomView++; } if (bottomView >= childViews.length) { bottomView = childViews.length - 1; } // If our controller has a `sawObjects` method, pass the on screen objects to it. var controller = this.get('controller'); if (onscreen.length) { this.setProperties({topVisible: onscreen[0], bottomVisible: onscreen[onscreen.length-1]}); if (controller && controller.sawObjects) { Em.run.schedule('afterRender', function() { controller.sawObjects(onscreen); }); } } else { this.setProperties({topVisible: null, bottomVisible: null}); } var toCloak = childViews.slice(0, topView).concat(childViews.slice(bottomView+1)), loadingView = childViews[bottomView + 1]; Em.run.schedule('afterRender', function() { toUncloak.forEach(function (v) { v.uncloak(); }); toCloak.forEach(function (v) { v.cloak(); }); }); for (var j=bottomView; j<childViews.length; j++) { var checkView = childViews[j]; if (!checkView.get('containedView')) { if (!checkView.get('loading')) { checkView.$().html("<div class='spinner'>" + I18n.t('loading') + "</div>"); } return; } } }, scrollTriggered: function() { Em.run.scheduleOnce('afterRender', this, 'scrolled'); }, didInsertElement: function() { var self = this, onScrollMethod = function() { Ember.run.debounce(self, 'scrollTriggered', 10); }; $(document).bind('touchmove.ember-cloak', onScrollMethod); $(window).bind('scroll.ember-cloak', onScrollMethod); }, willDestroyElement: function() { $(document).bind('touchmove.ember-cloak'); $(window).bind('scroll.ember-cloak'); } }); /** A cloaked view is one that removes its content when scrolled off the screen @class CloakedView @extends Ember.View @namespace Ember **/ Ember.CloakedView = Ember.View.extend({ attributeBindings: ['style'], init: function() { this._super(); this.uncloak(); }, /** Triggers the set up for rendering a view that is cloaked. @method uncloak */ uncloak: function() { var containedView = this.get('containedView'); if (!containedView) { this.setProperties({ style: null, loading: false, containedView: this.createChildView(this.get('cloaks'), {content: this.get('content') }) }); this.rerender(); } }, /** Removes the view from the DOM and tears down all observers. @method cloak */ cloak: function() { var containedView = this.get('containedView'), self = this; if (containedView && this.get('state') === 'inDOM') { var style = 'height: ' + this.$().height() + 'px;'; this.set('style', style); this.$().prop('style', style); // We need to remove the container after the height of the element has taken // effect. Ember.run.schedule('afterRender', function() { self.set('containedView', null); containedView.willDestroyElement(); containedView.remove(); }); } }, /** Render the cloaked view if applicable. @method render */ render: function(buffer) { var containedView = this.get('containedView'); if (containedView && containedView.get('state') !== 'inDOM') { containedView.renderToBuffer(buffer); containedView.transitionTo('inDOM'); Em.run.schedule('afterRender', function() { containedView.didInsertElement(); }); } } }); Ember.Handlebars.registerHelper('cloaked-collection', function(options) { var hash = options.hash, types = options.hashTypes; for (var prop in hash) { if (types[prop] === 'ID') { hash[prop + 'Binding'] = hash[prop]; delete hash[prop]; } } return Ember.Handlebars.helpers.view.call(this, Ember.CloakedCollectionView, options); }); })();