From 4e46fa1c6ee65c10776bcfaa610d3684b9ea69f3 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sat, 28 Apr 2012 04:07:17 +0200 Subject: [PATCH] Added infinite scrolling. --- .../controllers/agent_ticket_view.js.coffee | 15 +- app/assets/javascripts/app/index.js.coffee | 22 +- app/assets/javascripts/app/lib/waypoints.js | 674 ++++++++++++++++++ .../app/views/agent_ticket_view.jst.eco | 30 +- 4 files changed, 709 insertions(+), 32 deletions(-) create mode 100644 app/assets/javascripts/app/lib/waypoints.js diff --git a/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee index 9610282bc..d6136f9ad 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee @@ -31,6 +31,7 @@ class Index extends App.Controller fetch: -> + # get data @ajax = new App.Ajax @ajax.ajax( @@ -68,7 +69,7 @@ class Index extends App.Controller @loadCollection( type: 'Ticket', data: data.tickets ) # remember ticket order - @tickets = data.tickets + @tickets = @tickets.concat( data.tickets ) # remember ticket count @tickets_count = data.tickets_count @@ -87,6 +88,7 @@ class Index extends App.Controller render: -> + # get total pages pages_total = parseInt( ( @tickets_count / @overview.view[@view_mode].per_page ) + 0.99999 ) || 1 # render init page @@ -111,7 +113,7 @@ class Index extends App.Controller ) html = $(html) # html.find('li').removeClass('active') - html.find("[data-id=\"#{@start_page}\"]").parents('li').addClass('active') +# html.find("[data-id=\"#{@start_page}\"]").parents('li').addClass('active') @html html # create table/overview @@ -159,6 +161,15 @@ class Index extends App.Controller @el.find('.bulk-action').removeClass('hide') ) + # set waypoint if not already at the end + if @start_page < pages_total + a = => +# alert('You have scrolled to an entry.') + @start_page = @start_page + 1 + @fetch() + + $('footer').waypoint( a, { offset: '120%', triggerOnce: true } ) + page: (e) => e.preventDefault() id = $(e.target).data('id') diff --git a/app/assets/javascripts/app/index.js.coffee b/app/assets/javascripts/app/index.js.coffee index 097a701e4..1b41bc2bb 100644 --- a/app/assets/javascripts/app/index.js.coffee +++ b/app/assets/javascripts/app/index.js.coffee @@ -16,6 +16,7 @@ #= require ./lib/ba-linkify.js #= require ./lib/jquery.tagsinput.js #= require ./lib/jquery.noty.js +#= require ./lib/waypoints.js #= require ./lib/fileuploader.js #not_used= require_tree ./lib @@ -159,29 +160,19 @@ class App.Run extends Spine.Controller # start content new App.Content( el: @el.find('#content') ); -#class App.Content extends Spine.Stack class App.Content extends Spine.Controller className: 'container' constructor: -> -# @controllers = {} -# @routes = {} -# @default = '/' -# for route, controller of Config.Routes -## @log 'route,controller', route, controller -# @controllers[route] = controller -# @routes[route] = route - super @log 'RUN content'#, @ for route, callback of Config.Routes -# @log 'route,controller', route#, controller do (route, callback) => @route(route, (params) -> -# @log 'routing...', route Config['ActiveController'] = route - + Spine.trigger( 'ws:send', JSON.stringify( { action: 'active_controller', controller: route, params: params } ) ) + # unbind in controller area @el.unbind() @el.undelegate() @@ -194,11 +185,6 @@ class App.Content extends Spine.Controller # window.scrollTo(0,0) ) -# for name, object of Config.Controller -## @log 'new', object, @el -# new object( el: @el, auth: @auth ) - Spine.Route.setup() -window.App = App - +window.App = App \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/waypoints.js b/app/assets/javascripts/app/lib/waypoints.js new file mode 100644 index 000000000..450903cf7 --- /dev/null +++ b/app/assets/javascripts/app/lib/waypoints.js @@ -0,0 +1,674 @@ +/*! +jQuery Waypoints - v1.1.6 +Copyright (c) 2011-2012 Caleb Troughton +Dual licensed under the MIT license and GPL license. +https://github.com/imakewebthings/jquery-waypoints/blob/master/MIT-license.txt +https://github.com/imakewebthings/jquery-waypoints/blob/master/GPL-license.txt +*/ + +/* +Waypoints is a small jQuery plugin that makes it easy to execute a function +whenever you scroll to an element. + +GitHub Repository: https://github.com/imakewebthings/jquery-waypoints +Documentation and Examples: http://imakewebthings.github.com/jquery-waypoints + +Changelog: + v1.1.6 + - Fix potential memory leak by unbinding events on empty context elements. + v1.1.5 + - Make plugin compatible with Browserify/RequireJS. (Thanks @cjroebuck) + v1.1.4 + - Add handler option to give alternate binding method. (Issue #34) + v1.1.3 + - Fix cases where waypoints are added post-load and should be triggered + immediately. (Issue #28) + v1.1.2 + - Fixed error thrown by waypoints with triggerOnce option that were + triggered via resize refresh. + v1.1.1 + - Fixed bug in initialization where all offsets were being calculated + as if set to 0 initially, causing unwarranted triggers during the + subsequent refresh. + - Added onlyOnScroll, an option for individual waypoints that disables + triggers due to an offset refresh that crosses the current scroll + point. (All credit to @knuton on this one.) + v1.1 + - Moved the continuous option out of global settings and into the options + object for individual waypoints. + - Added the context option, which allows for using waypoints within any + scrollable element, not just the window. + v1.0.2 + - Moved scroll and resize handler bindings out of load. Should play nicer + with async loaders like Head JS and LABjs. + - Fixed a 1px off error when using certain % offsets. + - Added unit tests. + v1.0.1 + - Added $.waypoints('viewportHeight'). + - Fixed iOS bug (using the new viewportHeight method). + - Added offset function alias: 'bottom-in-view'. + v1.0 + - Initial release. + +Support: + - jQuery versions 1.4.3+ + - IE6+, FF3+, Chrome 6+, Safari 4+, Opera 11 + - Other versions and browsers may work, these are just the ones I've looked at. +*/ + +(function($, wp, wps, window, undefined){ + '$:nomunge'; + + var $w = $(window), + + // Keeping common strings as variables = better minification + eventName = 'waypoint.reached', + + /* + For the waypoint and direction passed in, trigger the waypoint.reached + event and deal with the triggerOnce option. + */ + triggerWaypoint = function(way, dir) { + way.element.trigger(eventName, dir); + if (way.options.triggerOnce) { + way.element[wp]('destroy'); + } + }, + + /* + Given a jQuery element and Context, returns the index of that element in the waypoints + array. Returns the index, or -1 if the element is not a waypoint. + */ + waypointIndex = function(el, context) { + if (!context) return -1; + var i = context.waypoints.length - 1; + while (i >= 0 && context.waypoints[i].element[0] !== el[0]) { + i -= 1; + } + return i; + }, + + // Private list of all elements used as scrolling contexts for waypoints. + contexts = [], + + /* + Context Class - represents a scrolling context. Properties include: + element: jQuery object containing a single HTML element. + waypoints: Array of waypoints operating under this scroll context. + oldScroll: Keeps the previous scroll position to determine scroll direction. + didScroll: Flag used in scrolling the context's scroll event. + didResize: Flag used in scrolling the context's resize event. + doScroll: Function that checks for crossed waypoints. Called from throttler. + */ + Context = function(context) { + $.extend(this, { + element: $(context), + oldScroll: 0, + + /* + List of all elements that have been registered as waypoints. + Each object in the array contains: + element: jQuery object containing a single HTML element. + offset: The window scroll offset, in px, that triggers the waypoint event. + options: Options object that was passed to the waypoint fn function. + */ + 'waypoints': [], + + didScroll: false, + didResize: false, + + doScroll: $.proxy(function() { + var newScroll = this.element.scrollTop(), + + // Are we scrolling up or down? Used for direction argument in callback. + isDown = newScroll > this.oldScroll, + that = this, + + // Get a list of all waypoints that were crossed since last scroll move. + pointsHit = $.grep(this.waypoints, function(el, i) { + return isDown ? + (el.offset > that.oldScroll && el.offset <= newScroll) : + (el.offset <= that.oldScroll && el.offset > newScroll); + }), + len = pointsHit.length; + + // iOS adjustment + if (!this.oldScroll || !newScroll) { + $[wps]('refresh'); + } + + // Done with scroll comparisons, store new scroll before ejection + this.oldScroll = newScroll; + + // No waypoints crossed? Eject. + if (!len) return; + + // If several waypoints triggered, need to do so in reverse order going up + if (!isDown) pointsHit.reverse(); + + /* + One scroll move may cross several waypoints. If the waypoint's continuous + option is true it should fire even if it isn't the last waypoint. If false, + it will only fire if it's the last one. + */ + $.each(pointsHit, function(i, point) { + if (point.options.continuous || i === len - 1) { + triggerWaypoint(point, [isDown ? 'down' : 'up']); + } + }); + }, this) + }); + + // Setup scroll and resize handlers. Throttled at the settings-defined rate limits. + $(context).bind('scroll.waypoints', $.proxy(function() { + if (!this.didScroll) { + this.didScroll = true; + window.setTimeout($.proxy(function() { + this.doScroll(); + this.didScroll = false; + }, this), $[wps].settings.scrollThrottle); + } + }, this)).bind('resize.waypoints', $.proxy(function() { + if (!this.didResize) { + this.didResize = true; + window.setTimeout($.proxy(function() { + $[wps]('refresh'); + this.didResize = false; + }, this), $[wps].settings.resizeThrottle); + } + }, this)); + + $w.load($.proxy(function() { + /* + Fire a scroll check, should the page be loaded at a non-zero scroll value, + as with a fragment id link or a page refresh. + */ + this.doScroll(); + }, this)); + }, + + /* Returns a Context object from the contexts array, given the raw HTML element + for that context. */ + getContextByElement = function(element) { + var found = null; + + $.each(contexts, function(i, c) { + if (c.element[0] === element) { + found = c; + return false; + } + }); + + return found; + }, + + // Methods exposed to the effin' object + methods = { + /* + jQuery.fn.waypoint([handler], [options]) + + handler + function, optional + A callback function called when the user scrolls past the element. + The function signature is function(event, direction) where event is + a standard jQuery Event Object and direction is a string, either 'down' + or 'up' indicating which direction the user is scrolling. + + options + object, optional + A map of options to apply to this set of waypoints, including where on + the browser window the waypoint is triggered. For a full list of + options and their defaults, see $.fn.waypoint.defaults. + + This is how you register an element as a waypoint. When the user scrolls past + that element it triggers waypoint.reached, a custom event. Since the + parameters for creating a waypoint are optional, we have a few different + possible signatures. Let’s look at each of them. + + someElements.waypoint(); + + Calling .waypoint with no parameters will register the elements as waypoints + using the default options. The elements will fire the waypoint.reached event, + but calling it in this way does not bind any handler to the event. You can + bind to the event yourself, as with any other event, like so: + + someElements.bind('waypoint.reached', function(event, direction) { + // make it rain + }); + + You will usually want to create a waypoint and immediately bind a function to + waypoint.reached, and can do so by passing a handler as the first argument to + .waypoint: + + someElements.waypoint(function(event, direction) { + if (direction === 'down') { + // do this on the way down + } + else { + // do this on the way back up through the waypoint + } + }); + + This will still use the default options, which will trigger the waypoint when + the top of the element hits the top of the window. We can pass .waypoint an + options object to customize things: + + someElements.waypoint(function(event, direction) { + // do something amazing + }, { + offset: '50%' // middle of the page + }); + + You can also pass just an options object. + + someElements.waypoint({ + offset: 100 // 100px from the top + }); + + This behaves like .waypoint(), in that it registers the elements as waypoints + but binds no event handlers. + + Calling .waypoint on an existing waypoint will extend the previous options. + If the call includes a handler, it will be bound to waypoint.reached without + unbinding any other handlers. + */ + init: function(f, options) { + // Register each element as a waypoint, add to array. + this.each(function() { + var cElement = $.fn[wp].defaults.context, + context, + $this = $(this); + + // Default window context or a specific element? + if (options && options.context) { + cElement = options.context; + } + + // Find the closest element that matches the context + if (!$.isWindow(cElement)) { + cElement = $this.closest(cElement)[0]; + } + context = getContextByElement(cElement); + + // Not a context yet? Create and push. + if (!context) { + context = new Context(cElement); + contexts.push(context); + } + + // Extend default and preexisting options + var ndx = waypointIndex($this, context), + base = ndx < 0 ? $.fn[wp].defaults : context.waypoints[ndx].options, + opts = $.extend({}, base, options); + + // Offset aliases + opts.offset = opts.offset === "bottom-in-view" ? + function() { + var cHeight = $.isWindow(cElement) ? $[wps]('viewportHeight') + : $(cElement).height(); + return cHeight - $(this).outerHeight(); + } : opts.offset; + + // Update, or create new waypoint + if (ndx < 0) { + context.waypoints.push({ + 'element': $this, + 'offset': null, + 'options': opts + }); + } + else { + context.waypoints[ndx].options = opts; + } + + // Bind the function if it was passed in. + if (f) { + $this.bind(eventName, f); + } + // Bind the function in the handler option if it exists. + if (options && options.handler) { + $this.bind(eventName, options.handler); + } + }); + + // Need to re-sort+refresh the waypoints array after new elements are added. + $[wps]('refresh'); + + return this; + }, + + + /* + jQuery.fn.waypoint('remove') + + Passing the string 'remove' to .waypoint unregisters the elements as waypoints + and wipes any custom options, but leaves the waypoint.reached events bound. + Calling .waypoint again in the future would reregister the waypoint and the old + handlers would continue to work. + */ + remove: function() { + return this.each(function(i, el) { + var $el = $(el); + + $.each(contexts, function(i, c) { + var ndx = waypointIndex($el, c); + + if (ndx >= 0) { + c.waypoints.splice(ndx, 1); + + if (!c.waypoints.length) { + c.element.unbind('scroll.waypoints resize.waypoints'); + contexts.splice(i, 1); + } + } + }); + }); + }, + + /* + jQuery.fn.waypoint('destroy') + + Passing the string 'destroy' to .waypoint will unbind all waypoint.reached + event handlers on those elements and unregisters them as waypoints. + */ + destroy: function() { + return this.unbind(eventName)[wp]('remove'); + } + }, + + /* + Methods used by the jQuery object extension. + */ + jQMethods = { + + /* + jQuery.waypoints('refresh') + + This will force a recalculation of each waypoint’s trigger point based on + its offset option and context. This is called automatically whenever the window + (or other defined context) is resized, new waypoints are added, or a waypoint’s + options are modified. If your project is changing the DOM or page layout without + doing one of these things, you may want to manually call this refresh. + */ + refresh: function() { + $.each(contexts, function(i, c) { + var isWin = $.isWindow(c.element[0]), + contextOffset = isWin ? 0 : c.element.offset().top, + contextHeight = isWin ? $[wps]('viewportHeight') : c.element.height(), + contextScroll = isWin ? 0 : c.element.scrollTop(); + + $.each(c.waypoints, function(j, o) { + /* $.each isn't safe from element removal due to triggerOnce. + Should rewrite the loop but this is way easier. */ + if (!o) return; + + // Adjustment is just the offset if it's a px value + var adjustment = o.options.offset, + oldOffset = o.offset; + + // Set adjustment to the return value if offset is a function. + if (typeof o.options.offset === "function") { + adjustment = o.options.offset.apply(o.element); + } + // Calculate the adjustment if offset is a percentage. + else if (typeof o.options.offset === "string") { + var amount = parseFloat(o.options.offset); + adjustment = o.options.offset.indexOf("%") ? + Math.ceil(contextHeight * (amount / 100)) : amount; + } + + /* + Set the element offset to the window scroll offset, less + all our adjustments. + */ + o.offset = o.element.offset().top - contextOffset + + contextScroll - adjustment; + + /* + An element offset change across the current scroll point triggers + the event, just as if we scrolled past it unless prevented by an + optional flag. + */ + if (o.options.onlyOnScroll) return; + + if (oldOffset !== null && c.oldScroll > oldOffset && c.oldScroll <= o.offset) { + triggerWaypoint(o, ['up']); + } + else if (oldOffset !== null && c.oldScroll < oldOffset && c.oldScroll >= o.offset) { + triggerWaypoint(o, ['down']); + } + /* For new waypoints added after load, check that down should have + already been triggered */ + else if (!oldOffset && c.element.scrollTop() > o.offset) { + triggerWaypoint(o, ['down']); + } + }); + + // Keep waypoints sorted by offset value. + c.waypoints.sort(function(a, b) { + return a.offset - b.offset; + }); + }); + }, + + + /* + jQuery.waypoints('viewportHeight') + + This will return the height of the viewport, adjusting for inconsistencies + that come with calling $(window).height() in iOS. Recommended for use + within any offset functions. + */ + viewportHeight: function() { + return (window.innerHeight ? window.innerHeight : $w.height()); + }, + + + /* + jQuery.waypoints() + + This will return a jQuery object with a collection of all registered waypoint + elements. + + $('.post').waypoint(); + $('.ad-unit').waypoint(function(event, direction) { + // Passed an ad unit + }); + console.log($.waypoints()); + + The example above would log a jQuery object containing all .post and .ad-unit + elements. + */ + aggregate: function() { + var points = $(); + $.each(contexts, function(i, c) { + $.each(c.waypoints, function(i, e) { + points = points.add(e.element); + }); + }); + return points; + } + }; + + + /* + fn extension. Delegates to appropriate method. + */ + $.fn[wp] = function(method) { + + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } + else if (typeof method === "function" || !method) { + return methods.init.apply(this, arguments); + } + else if (typeof method === "object") { + return methods.init.apply(this, [null, method]); + } + else { + $.error( 'Method ' + method + ' does not exist on jQuery ' + wp ); + } + }; + + + /* + The default options object that is extended when calling .waypoint. It has the + following properties: + + context + string | element | jQuery* + default: window + The context defines which scrollable element the waypoint belongs to and acts + within. The default, window, means the waypoint offset is calculated with relation + to the whole viewport. You can set this to another element to use the waypoints + within that element. Accepts a selector string, *but if you use jQuery 1.6+ it + also accepts a raw HTML element or jQuery object. + + continuous + boolean + default: true + If true, and multiple waypoints are triggered in one scroll, this waypoint will + trigger even if it is not the last waypoint reached. If false, it will only + trigger if it is the last waypoint. + + handler + function + default: undefined + An alternative way to bind functions to the waypoint, without using the function + as the first argument to the waypoint function. + + offset + number | string | function + default: 0 + Determines how far the top of the element must be from the top of the browser + window to trigger a waypoint. It can be a number, which is taken as a number + of pixels, a string representing a percentage of the viewport height, or a + function that will return a number of pixels. + + onlyOnScroll + boolean + default: false + If true, this waypoint will not trigger if an offset change during a refresh + causes it to pass the current scroll point. + + triggerOnce + boolean + default: false + If true, the waypoint will be destroyed when triggered. + + An offset of 250 would trigger the waypoint when the top of the element is 250px + from the top of the viewport. Negative values for any offset work as you might + expect. A value of -100 would trigger the waypoint when the element is 100px above + the top of the window. + + offset: '100%' + + A string percentage will determine the pixel offset based on the height of the + window. When resizing the window, this offset will automatically be recalculated + without needing to call $.waypoints('refresh'). + + // The bottom of the element is in view + offset: function() { + return $.waypoints('viewportHeight') - $(this).outerHeight(); + } + + Offset can take a function, which must return a number of pixels from the top of + the window. The this value will always refer to the raw HTML element of the + waypoint. As with % values, functions are recalculated automatically when the + window resizes. For more on recalculating offsets, see $.waypoints('refresh'). + + An offset value of 'bottom-in-view' will act as an alias for the function in the + example above, as this is a common usage. + + offset: 'bottom-in-view' + + You can see this alias in use on the Scroll Analytics example page. + + The triggerOnce flag, if true, will destroy the waypoint after the first trigger. + This is just a shortcut for calling .waypoint('destroy') within the waypoint + handler. This is useful in situations such as scroll analytics, where you only + want to record an event once for each page visit. + + The context option lets you use Waypoints within an element other than the window. + You can define the context with a selector string and the waypoint will act within + the nearest ancestor that matches this selector. + + $('.something-scrollable .waypoint').waypoint({ + context: '.something-scrollable' + }); + + You can see this in action on the Dial Controls example. + + The handler option gives authors an alternative way to bind functions when + creating a waypoint. In place of: + + $('.item').waypoint(function(event, direction) { + // make things happen + }); + + You may instead write: + + $('.item').waypoint({ + handler: function(event, direction) { + // make things happen + } + }); + + */ + $.fn[wp].defaults = { + continuous: true, + offset: 0, + triggerOnce: false, + context: window + }; + + + + + + /* + jQuery object extension. Delegates to appropriate methods above. + */ + $[wps] = function(method) { + if (jQMethods[method]) { + return jQMethods[method].apply(this); + } + else { + return jQMethods['aggregate'](); + } + }; + + + /* + $.waypoints.settings + + Settings object that determines some of the plugin’s behavior. + + resizeThrottle + number + default: 200 + For performance reasons, the refresh performed during resizes is + throttled. This value is the rate-limit in milliseconds between resize + refreshes. For more information on throttling, check out Ben Alman’s + throttle / debounce plugin. + http://benalman.com/projects/jquery-throttle-debounce-plugin/ + + scrollThrottle + number + default: 100 + For performance reasons, checking for any crossed waypoints during a + scroll event is throttled. This value is the rate-limit in milliseconds + between scroll checks. For more information on throttling, check out Ben + Alman’s throttle / debounce plugin. + http://benalman.com/projects/jquery-throttle-debounce-plugin/ + */ + $[wps].settings = { + resizeThrottle: 200, + scrollThrottle: 100 + }; + + $w.load(function() { + // Calculate everything once on load. + $[wps]('refresh'); + }); +})(jQuery, 'waypoint', 'waypoints', window); diff --git a/app/assets/javascripts/app/views/agent_ticket_view.jst.eco b/app/assets/javascripts/app/views/agent_ticket_view.jst.eco index d2f0c68fb..ee225c0c3 100644 --- a/app/assets/javascripts/app/views/agent_ticket_view.jst.eco +++ b/app/assets/javascripts/app/views/agent_ticket_view.jst.eco @@ -1,7 +1,22 @@ - +