You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by jo...@apache.org on 2014/01/23 18:30:04 UTC

[22/28] git commit: [#4257] Added PJAX Show More paging with scroll state saving

[#4257] Added PJAX Show More paging with scroll state saving

Signed-off-by: Cory Johns <cj...@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/df82f288
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/df82f288
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/df82f288

Branch: refs/heads/cj/4257
Commit: df82f2887de3791b8070b12402791a98da454d0c
Parents: 28f74b1
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Sat Jan 18 00:50:38 2014 +0000
Committer: Cory Johns <cj...@slashdotmedia.com>
Committed: Thu Jan 23 17:28:03 2014 +0000

----------------------------------------------------------------------
 Allura/LICENSE                                  |   2 +
 Allura/allura/public/nf/js/jquery.viewport.js   |  58 +++++
 ForgeActivity/forgeactivity/main.py             |   4 +
 .../forgeactivity/nf/activity/css/activity.css  |  10 +
 .../forgeactivity/nf/activity/js/activity.js    | 209 +++++++++++++++++++
 .../forgeactivity/templates/index.html          |  21 +-
 .../forgeactivity/templates/timeline.html       |  35 ++++
 7 files changed, 324 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/df82f288/Allura/LICENSE
----------------------------------------------------------------------
diff --git a/Allura/LICENSE b/Allura/LICENSE
index 7e0be3d..36c9038 100644
--- a/Allura/LICENSE
+++ b/Allura/LICENSE
@@ -232,6 +232,8 @@ under the MIT license.  For details, see the individual files:
     allura/lib/widgets/resources/js/jquery.tools.min.js
     allura/public/nf/js/jquery.flot.js
     allura/public/nf/js/jquery.maxlength.min.js
+    allura/public/nf/js/jquery.viewport.js
+    allura/public/nf/js/jquery.pjax.js
 
 Blueprint, which is available under the MIT license.
 For details, see allura/public/nf/css/blueprint/

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/df82f288/Allura/allura/public/nf/js/jquery.viewport.js
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/js/jquery.viewport.js b/Allura/allura/public/nf/js/jquery.viewport.js
new file mode 100644
index 0000000..7826000
--- /dev/null
+++ b/Allura/allura/public/nf/js/jquery.viewport.js
@@ -0,0 +1,58 @@
+/*
+ * Viewport - jQuery selectors for finding elements in viewport
+ *
+ * Copyright (c) 2008-2009 Mika Tuupola
+ *
+ * Licensed under the MIT license:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *
+ * Project home:
+ *  http://www.appelsiini.net/projects/viewport
+ *
+ */
+(function($) {
+    
+    $.belowthefold = function(element, settings) {
+        var fold = $(window).height() + $(window).scrollTop();
+        return fold <= $(element).offset().top - settings.threshold;
+    };
+
+    $.abovethetop = function(element, settings) {
+        var top = $(window).scrollTop();
+        return top >= $(element).offset().top + $(element).height() - settings.threshold;
+    };
+    
+    $.rightofscreen = function(element, settings) {
+        var fold = $(window).width() + $(window).scrollLeft();
+        return fold <= $(element).offset().left - settings.threshold;
+    };
+    
+    $.leftofscreen = function(element, settings) {
+        var left = $(window).scrollLeft();
+        return left >= $(element).offset().left + $(element).width() - settings.threshold;
+    };
+    
+    $.inviewport = function(element, settings) {
+        return !$.rightofscreen(element, settings) && !$.leftofscreen(element, settings) && !$.belowthefold(element, settings) && !$.abovethetop(element, settings);
+    };
+    
+    $.extend($.expr[':'], {
+        "below-the-fold": function(a, i, m) {
+            return $.belowthefold(a, {threshold : 0});
+        },
+        "above-the-top": function(a, i, m) {
+            return $.abovethetop(a, {threshold : 0});
+        },
+        "left-of-screen": function(a, i, m) {
+            return $.leftofscreen(a, {threshold : 0});
+        },
+        "right-of-screen": function(a, i, m) {
+            return $.rightofscreen(a, {threshold : 0});
+        },
+        "in-viewport": function(a, i, m) {
+            return $.inviewport(a, {threshold : 0});
+        }
+    });
+
+    
+})(jQuery);

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/df82f288/ForgeActivity/forgeactivity/main.py
----------------------------------------------------------------------
diff --git a/ForgeActivity/forgeactivity/main.py b/ForgeActivity/forgeactivity/main.py
index 991bb61..09d0c62 100644
--- a/ForgeActivity/forgeactivity/main.py
+++ b/ForgeActivity/forgeactivity/main.py
@@ -119,6 +119,10 @@ class ForgeActivityController(BaseController):
     def index(self, **kw):
         return self._get_activities_data(**kw)
 
+    @expose('jinja:forgeactivity:templates/timeline.html')
+    def pjax(self, **kw):
+        return self._get_activities_data(**kw)
+
     @without_trailing_slash
     @expose()
     def feed(self, **kw):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/df82f288/ForgeActivity/forgeactivity/nf/activity/css/activity.css
----------------------------------------------------------------------
diff --git a/ForgeActivity/forgeactivity/nf/activity/css/activity.css b/ForgeActivity/forgeactivity/nf/activity/css/activity.css
index dc71608..66395ff 100644
--- a/ForgeActivity/forgeactivity/nf/activity/css/activity.css
+++ b/ForgeActivity/forgeactivity/nf/activity/css/activity.css
@@ -46,3 +46,13 @@
   float: left;
   margin: 10px 10px 0 0;
 }
+.activity .page_list {
+    margin-top: 5px;
+}
+.activity .show-more {
+    display: block;
+    text-align: center;
+}
+.activity .show-more.older {
+    margin-top: 10px;
+}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/df82f288/ForgeActivity/forgeactivity/nf/activity/js/activity.js
----------------------------------------------------------------------
diff --git a/ForgeActivity/forgeactivity/nf/activity/js/activity.js b/ForgeActivity/forgeactivity/nf/activity/js/activity.js
new file mode 100644
index 0000000..e8c0346
--- /dev/null
+++ b/ForgeActivity/forgeactivity/nf/activity/js/activity.js
@@ -0,0 +1,209 @@
+/*
+       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.
+*/
+
+ActivityBrowseOptions = {
+    maintainScrollState: true,
+    usePjax: true,
+    useHash: true,
+    forceAdvancedScroll: false,
+    useShowMore: true,
+    useInfiniteScroll: false
+}
+
+$(function() {
+    var hasAPI = window.history && window.history.pushState && window.history.replaceState;
+    var iOS4 = navigator.userAgent.match(/iP(od|one|ad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork/);
+    if (!hasAPI || iOS4) {
+        ActivityBrowseOptions.usePjax = false;
+    }
+    if (!ActivityBrowseOptions.usePjax) {
+        if (!ActivityBrowseOptions.useHash) {
+            ActivityBrowseOptions.maintainScrollState = false;
+        }
+        if (!ActivityBrowseOptions.forceAdvancedScroll) {
+            ActivityBrowseOptions.useShowMore = false;
+            ActivityBrowseOptions.useInfiniteScroll = false;
+        }
+    }
+
+    var firstVisibleId = null;
+    var oldScrollTop = null;
+    var oldTop = null;
+    function saveScrollPosition() {
+        var $firstVisible = $('.timeline li:in-viewport:first');
+        firstVisibleId = $firstVisible.attr('id');
+        oldScrollTop = $(window).scrollTop();
+        oldTop = $firstVisible.offset().top;
+    }
+    function restoreScrollPosition() {
+        var $window = $(window);
+        var $firstVisible = $('#'+firstVisibleId);
+        if (!$firstVisible.length) {
+            return;
+        }
+        var newTop = $firstVisible.offset().top;
+        var scrollTop = $window.scrollTop();
+        var elemAdjustment = newTop - oldTop;
+        var viewportAdjustment = scrollTop - oldScrollTop;
+        $window.scrollTop(scrollTop + elemAdjustment - viewportAdjustment);
+        //console.log('restoreSP', oldTop, newTop, elemAdjustment, viewportAdjustment, scrollTop, $window.scrollTop());
+        $(window).trigger('scroll');
+    }
+
+    function maintainScrollState_pjax() {
+        var $firstVisibleActivity = $('.timeline li:in-viewport:first');
+        var page = $firstVisibleActivity.data('page');
+        var limit = $('.timeline').data('limit');
+        var hash = $firstVisibleActivity.attr('id');
+        if (page != null && limit != null && hash != null) {
+            history.replaceState(null, null, '?page='+page+'&limit='+limit+'#'+hash);
+        }
+    }
+
+    function maintainScrollState_hash() {
+        var $firstVisibleActivity = $('.timeline li:in-viewport:first');
+        saveScrollPosition();
+        window.location.hash = $firstVisibleActivity.attr('id');  // causes jump...
+        restoreScrollPosition();
+    }
+
+    var delayed = null;
+    function scrollHandler(event) {
+        clearTimeout(delayed);
+        delayed = setTimeout(ActivityBrowseOptions.usePjax
+            ? maintainScrollState_pjax
+            : maintainScrollState_hash, 100);
+    }
+
+    function maintainScrollState() {
+        if (!ActivityBrowseOptions.maintainScrollState) {
+            return;
+        }
+        $(window).scroll(scrollHandler);
+    }
+
+    function pageOut(newer) {
+        var $timeline = $('.timeline li');
+        var limit = $('.timeline').data('limit') || 100;
+        var range = newer ? [0, limit] : [-limit, undefined];
+        $timeline.slice(range[0], range[1]).remove();
+        if (!newer && $('.show-more.older').hasClass('no-more')) {
+            $('.no-more').removeClass('no-more');
+            updateShowMore();
+        }
+    }
+    window.pageOut = pageOut;
+
+    function pageIn(newer, url) {
+        $.get(url, function(html) {
+            saveScrollPosition();
+            if (newer) {
+                $('.timeline').prepend(html);
+            } else {
+                if (html.match(/^\s*$/)) {
+                    $('.show-more.older').addClass('no-more');
+                } else {
+                    $('.timeline').append(html);
+                }
+            }
+            var firstPage = $('.timeline li:first').data('page');
+            var lastPage = $('.timeline li:last').data('page');
+            if (lastPage - firstPage >= 3) {
+                pageOut(!newer);
+            }
+            if (ActivityBrowseOptions.useShowMore) {
+                updateShowMore();
+            }
+            restoreScrollPosition();
+        }).fail(function() {
+            flash('Error loading activities', 'error');
+        });
+    }
+
+    function makeShowMoreHandler(newer) {
+        // has to be factory to prevent closure memory leak
+        // see: https://www.meteor.com/blog/2013/08/13/an-interesting-kind-of-javascript-memory-leak
+        return function(event) {
+            event.preventDefault();
+            pageIn(newer, this.href);
+        };
+    }
+
+    function makeShowMoreLink(newer, targetPage, limit) {
+        var $link = $('<a class="show-more">Show more</a>');
+        $link.addClass(newer ? 'newer' : 'older');
+        $link.attr('href', 'pjax?page='+targetPage+'&limit='+limit);
+        $link.click(makeShowMoreHandler(newer));  // has to be factory to prevent closure memory leak
+        return $link;
+    }
+
+    function updateShowMore() {
+        var $timeline = $('.timeline');
+        if (!$timeline.length) {
+            return;
+        }
+        var noMoreActivities = $('.show-more.older').hasClass('no-more');
+        $('.page_list, .show-more').remove();
+        var limit = $('.timeline').data('limit');
+        var firstPage = $('.timeline li:first').data('page');
+        var lastPage = $('.timeline li:last').data('page');
+        if (firstPage > 0) {
+            $timeline.before(makeShowMoreLink(true, firstPage-1, limit));
+        }
+        if (noMoreActivities) {
+            $timeline.after('<div class="show-more older no-more">No more activities</div>');
+        } else {
+            $timeline.after(makeShowMoreLink(false, lastPage+1, limit));
+        }
+    }
+    window.updateShowMore = updateShowMore;
+
+    function enableInfiniteScroll() {
+    }
+
+    function enableAdvancedPaging() {
+        if (ActivityBrowseOptions.useInfiniteScroll) {
+            enableInfiniteScroll();
+        } else if (ActivityBrowseOptions.useShowMore) {
+            updateShowMore();
+        }
+    }
+
+    maintainScrollState();  // http://xkcd.com/1309/
+    enableAdvancedPaging();
+});
+
+function markTop() {
+    var $marker = $('#offset-marker');
+    if (!$marker.length) {
+        $marker = $('<div id="offset-marker">&nbsp;</div>');
+        $marker.css({
+            'position': 'absolute',
+            'top': 0,
+            'width': '100%',
+            'border-top': '1px solid green'
+        });
+        $marker.appendTo($('body'));
+    }
+    $marker.css({'top': $(window).scrollTop()});
+}
+
+function markFirst() {
+    $('.timeline li:in-viewport:first').css({'background-color': '#f0fff0'});
+}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/df82f288/ForgeActivity/forgeactivity/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeActivity/forgeactivity/templates/index.html b/ForgeActivity/forgeactivity/templates/index.html
index e63c904..4b8c162 100644
--- a/ForgeActivity/forgeactivity/templates/index.html
+++ b/ForgeActivity/forgeactivity/templates/index.html
@@ -38,27 +38,18 @@
 {% endblock %}
 
 {% block content %}
+{% do g.register_forge_js('js/jquery.viewport.js') %}
+{% do g.register_app_js('js/activity.js') %}
+
 <div class="activity">
   {% if not timeline %}
     No activity to display.
   {% else %}
-    <ul class="timeline">
-        {% for a in timeline %}
-        <li>
-          <time datetime="{{a.published|datetimeformat}}" title="{{a.published|datetimeformat}}">{{h.ago(a.published, show_date_after=None)}}</time>
-          <h1>
-              {{ am.icon(a.actor, 32, 'avatar') }}
-              {{am.activity_obj(a.actor)}} {{a.verb}} {{am.activity_obj(a.obj)}} {% if a.target.activity_name %}on {{am.activity_obj(a.target)}}{% endif %}
-          </h1>
-          {% if a.obj.activity_extras.get('summary') %}
-          <p>
-            {{ a.obj.activity_extras.get('summary') }}
-          </p>
-          {% endif %}
-        </li>
-        {% endfor %}
+    <ul class="timeline" data-limit="{{limit}}">
+        {% include 'forgeactivity:templates/timeline.html' %}
         {{c.page_list.display(limit=1, page=page, count=page+1, show_label=False, show_if_single_page=True, force_next=True)}}
     </ul>
+    {{c.page_list.display(limit=1, page=page, count=page+1, show_label=False, show_if_single_page=True, force_next=True)}}
   {% endif %}
 </div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/df82f288/ForgeActivity/forgeactivity/templates/timeline.html
----------------------------------------------------------------------
diff --git a/ForgeActivity/forgeactivity/templates/timeline.html b/ForgeActivity/forgeactivity/templates/timeline.html
new file mode 100644
index 0000000..499fe03
--- /dev/null
+++ b/ForgeActivity/forgeactivity/templates/timeline.html
@@ -0,0 +1,35 @@
+{#-
+       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.
+-#}
+
+{% import 'forgeactivity:templates/macros.html' as am with context %}
+
+{% for a in timeline %}
+<li id="{{a._id}}" data-page="{{page}}">
+  <time datetime="{{a.published|datetimeformat}}" title="{{a.published|datetimeformat}}">{{h.ago(a.published, show_date_after=None)}}</time>
+  <h1>
+      {{ am.icon(a.actor, 32, 'avatar') }}
+      {{am.activity_obj(a.actor)}} {{a.verb}} {{am.activity_obj(a.obj)}} {% if a.target.activity_name %}on {{am.activity_obj(a.target)}}{% endif %}
+  </h1>
+  {% if a.obj.activity_extras.get('summary') %}
+  <p>
+    {{ a.obj.activity_extras.get('summary') }}
+  </p>
+  {% endif %}
+</li>
+{% endfor %}