/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var App = require('app'); /** * @typedef {Object} InfiniteScrollMixinOptions * @property {String} appendHtml html to append when scroll ends and callback executed. It's common * that root node has unique id or class attributes. * @property {Function} callback function to execute when scroll ends. This function should return * $.Deferred().promise() instance * @property {Function} onReject function to execute when callback rejected. */ /** * @mixin App.InfiniteScrollMixin * This mixin provides methods to attach infinite scroll to specific scrollable element. * * Usage: * * // mix it * var myView = Em.View.extend(App.InfiniteScrollMixin, { * didInsertElement: function() { * // call method infiniteScrollInit * this.infiniteScrollInit($('.some-scrollable'), { * callback: this.someCallbacOnEndReached * }); * } * }); * * */ App.InfiniteScrollMixin = Ember.Mixin.create({ /** * Stores callback execution progress. * * @type {Boolean} */ _infiniteScrollCallbackInProgress: false, /** * Stores HTMLElement infinite scroll initiated on. * * @type {HTMLElement} */ _infiniteScrollEl: null, /** * Default options for infinite scroll. * * @type {InfiniteScrollMixinOptions} */ _infiniteScrollDefaults: { appendHtml: '
', callback: function() { return $.Deferred().resolve().promise(); }, onReject: function() {}, onResolve: function() {} }, /** * Determines that there is no data to load on next callback call. * */ _infiniteScrollMoreData: true, /** * Initialize infinite scroll on specified HTMLElement. * * @param {HTMLElement} el DOM element to attach infinite scroll. * @param {InfiniteScrollMixinOptions} opts */ infiniteScrollInit: function(el, opts) { var options = $.extend({}, this.get('_infiniteScrollDefaults'), opts || {}); this.set('_infiniteScrollEl', el); this.get('_infiniteScrollEl').on('scroll', this._infiniteScrollHandler.bind(this)); this.get('_infiniteScrollEl').on('infinite-scroll-end', this._infiniteScrollEndHandler(options).bind(this)); }, /** * Handler executed on scrolling. * @param {jQuery.Event} e */ _infiniteScrollHandler: function(e) { var el = $(e.target); var height = el.get(0).clientHeight; var scrollHeight = el.prop('scrollHeight'); var endPoint = scrollHeight - height; if (endPoint === el.scrollTop() && !this.get('_infiniteScrollCallbackInProgress')) { el.trigger('infinite-scroll-end'); } }, /** * Handler called when scroll ends. * * @param {InfiniteScrollMixinOptions} options * @return {Function} */ _infiniteScrollEndHandler: function(options) { return function(e) { var self = this; if (this.get('_infiniteScrollCallbackInProgress') || !this.get('_infiniteScrollMoreData')) return; this._infiniteScrollAppendHtml(options.appendHtml); // always scroll to bottom this.get('_infiniteScrollEl').scrollTop(this.get('_infiniteScrollEl').get(0).scrollHeight); this.set('_infiniteScrollCallbackInProgress', true); options.callback().then(function() { options.onResolve(); }, function() { options.onReject(); }).always(function() { self.set('_infiniteScrollCallbackInProgress', false); self._infiniteScrollRemoveHtml(options.appendHtml); }); }.bind(this); }, /** * Helper function to append String as html node to. * @param {String} htmlString string to append */ _infiniteScrollAppendHtml: function(htmlString) { this.get('_infiniteScrollEl').append(htmlString); }, /** * Remove HTMLElement by specified string that can be converted to html. HTMLElement root node * should have unique id or class attribute to avoid removing additional * elements. * * @param {String} htmlString string to remove */ _infiniteScrollRemoveHtml: function(htmlString) { this.get('_infiniteScrollEl').find(this._infiniteScrollGetSelector(htmlString)).remove(); }, /** * Get root node selector. * id attribute has higher priority and will return if found. * class if no id attribute found class attribute * will be used. * * @param {String} htmlString string processed as HTML * @return {[type]} [description] */ _infiniteScrollGetSelector: function(htmlString) { var html = $(htmlString); var elId = html.attr('id'); var elClass = (html.attr('class') || '').split(' ').join('.'); html = null; return !!elId ? '#' + elId : '.' + elClass; }, /** * Remove infinite scroll. * Unbind all listeners. */ infiniteScrollDestroy: function() { this.get('_infiniteScrollEl').off('scroll', this._infiniteScrollHandler); this.get('_infiniteScrollEl').off('infinite-scroll-end', this._infiniteScrollHandler); this.set('_infiniteScrollEl', null); }, /** * Set if there is more data to load on next scroll end event. * @param {boolean} isAvailable true when there are more data to fetch */ infiniteScrollSetDataAvailable: function(isAvailable) { this.set('_infiniteScrollMoreData', isAvailable); } });