[extensions-web] Make pagination client-side



commit b10198bcf17a897bdd67914ce50397de6acbb33f
Author: Jasper St. Pierre <jstpierre mecheye net>
Date:   Tue Dec 13 20:18:10 2011 -0500

    Make pagination client-side
    
    This will help sorting and filtering in the long run.

 .../extensions/templates/extensions/list.html      |   30 +--
 .../extensions/templates/extensions/list_bare.html |   17 +
 sweettooth/extensions/urls.py                      |    3 +-
 sweettooth/extensions/views.py                     |    7 +-
 sweettooth/static/js/jquery.hashchange.js          |  390 ++++++++++++++++++++
 sweettooth/static/js/main.js                       |    5 +-
 sweettooth/static/js/paginator.js                  |  149 ++++++++
 7 files changed, 567 insertions(+), 34 deletions(-)
---
diff --git a/sweettooth/extensions/templates/extensions/list.html b/sweettooth/extensions/templates/extensions/list.html
index 8681848..f6e658b 100644
--- a/sweettooth/extensions/templates/extensions/list.html
+++ b/sweettooth/extensions/templates/extensions/list.html
@@ -1,35 +1,7 @@
 {% extends "base.html" %}
 {% block body %}
-  {% load paginator %}
-  {% if page_obj.has_other_pages %}
-  <div class="paginator">
-    {% paginator page_obj %}
+  <div id="extensions-list">
   </div>
-  {% endif %}
-
-  {% if extension_list %}
-  <ul class="extensions">
-  {% for extension in extension_list %}
-    <li class="extension" data-svm="{{ extension.visible_shell_version_map_json }}">
-      <h3 class="extension-name"><a href="{% url extensions-detail pk=extension.pk %}" class="title-link"><img src="{{ extension.icon.url }}" class="icon">{{ extension.name }}</a></h3>
-      <span class="author">by <a href="{% url auth-profile user=extension.creator.username %}">{{ extension.creator }}</a></span>
-      <p class="description">
-        {{ extension.first_line_of_description }}
-      </p>
-    </li>
-  {% endfor %}
-  </ul>
-  {% else %}
-    <div class="empty">
-      There are no extensions.
-    </div>
-  {% endif %}
-
-  {% if page_obj.has_other_pages %}
-  <div class="paginator">
-    {% paginator page_obj %}
-  </div>
-  {% endif %}
 {% endblock %}
 
 {% block navclass %}main{% endblock %}
diff --git a/sweettooth/extensions/templates/extensions/list_bare.html b/sweettooth/extensions/templates/extensions/list_bare.html
new file mode 100644
index 0000000..11dd32a
--- /dev/null
+++ b/sweettooth/extensions/templates/extensions/list_bare.html
@@ -0,0 +1,17 @@
+{% if extension_list %}
+<ul class="extensions">
+  {% for extension in extension_list %}
+  <li class="extension" data-svm="{{ extension.visible_shell_version_map_json }}">
+    <h3 class="extension-name"><a href="{% url extensions-detail pk=extension.pk %}" class="title-link"><img src="{{ extension.icon.url }}" class="icon">{{ extension.name }}</a></h3>
+    <span class="author">by <a href="{% url auth-profile user=extension.creator.username %}">{{ extension.creator }}</a></span>
+    <p class="description">
+      {{ extension.first_line_of_description }}
+    </p>
+  </li>
+  {% endfor %}
+</ul>
+{% else %}
+<div class="empty">
+  There are no extensions.
+</div>
+{% endif %}
diff --git a/sweettooth/extensions/urls.py b/sweettooth/extensions/urls.py
index ff55693..41deaaf 100644
--- a/sweettooth/extensions/urls.py
+++ b/sweettooth/extensions/urls.py
@@ -21,6 +21,7 @@ ajax_patterns = patterns('',
         dict(newstatus=models.STATUS_ACTIVE), name='extensions-ajax-set-status-active'),
     url(r'^set-status/inactive/', views.ajax_set_status_view,
         dict(newstatus=models.STATUS_INACTIVE), name='extensions-ajax-set-status-inactive'),
+    url(r'^extensions-list/', views.ajax_extensions_list),
 )
 
 shell_patterns = patterns('',
@@ -35,7 +36,7 @@ shell_patterns = patterns('',
 )
 
 urlpatterns = patterns('',
-    url(r'^$', views.extensions_list, name='extensions-index'),
+    url(r'^$', direct_to_template, dict(template='extensions/list.html'), name='extensions-index'),
 
     url(r'^about/$', direct_to_template, dict(template='extensions/about.html'), name='extensions-about'),
 
diff --git a/sweettooth/extensions/views.py b/sweettooth/extensions/views.py
index 68d68e0..1d8dc85 100644
--- a/sweettooth/extensions/views.py
+++ b/sweettooth/extensions/views.py
@@ -75,7 +75,8 @@ def shell_update(request):
 
     return operations
 
-def extensions_list(request):
+ ajax_view
+def ajax_extensions_list(request):
     queryset = models.Extension.objects.visible()
     if request.GET.get('sort', '') == 'recent':
         queryset = queryset.order_by('-pk')
@@ -99,10 +100,10 @@ def extensions_list(request):
 
     context = dict(paginator=paginator,
                    page_obj=page_obj,
-                   is_paginated=page_obj.has_other_pages(),
                    extension_list=page_obj.object_list)
 
-    return render(request, 'extensions/list.html', context)
+    return dict(html=render_to_string('extensions/list_bare.html', context),
+                numpages = paginator.num_pages)
 
 @model_view(models.Extension)
 def extension_view(request, obj, **kwargs):
diff --git a/sweettooth/static/js/jquery.hashchange.js b/sweettooth/static/js/jquery.hashchange.js
new file mode 100644
index 0000000..47105f4
--- /dev/null
+++ b/sweettooth/static/js/jquery.hashchange.js
@@ -0,0 +1,390 @@
+/*!
+ * jQuery hashchange event - v1.3 - 7/21/2010
+ * http://benalman.com/projects/jquery-hashchange-plugin/
+ * 
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+
+// Script: jQuery hashchange event
+//
+// *Version: 1.3, Last updated: 7/21/2010*
+// 
+// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
+// GitHub       - http://github.com/cowboy/jquery-hashchange/
+// Source       - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
+// (Minified)   - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
+// 
+// About: License
+// 
+// Copyright (c) 2010 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+// 
+// About: Examples
+// 
+// These working examples, complete with fully commented code, illustrate a few
+// ways in which this plugin can be used.
+// 
+// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
+// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
+// 
+// About: Support and Testing
+// 
+// Information about what version or versions of jQuery this plugin has been
+// tested with, what browsers it has been tested in, and where the unit tests
+// reside (so you can test it yourself).
+// 
+// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
+// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
+//                   Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
+// Unit Tests      - http://benalman.com/code/projects/jquery-hashchange/unit/
+// 
+// About: Known issues
+// 
+// While this jQuery hashchange event implementation is quite stable and
+// robust, there are a few unfortunate browser bugs surrounding expected
+// hashchange event-based behaviors, independent of any JavaScript
+// window.onhashchange abstraction. See the following examples for more
+// information:
+// 
+// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
+// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
+// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
+// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
+// 
+// Also note that should a browser natively support the window.onhashchange 
+// event, but not report that it does, the fallback polling loop will be used.
+// 
+// About: Release History
+// 
+// 1.3   - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
+//         "removable" for mobile-only development. Added IE6/7 document.title
+//         support. Attempted to make Iframe as hidden as possible by using
+//         techniques from http://www.paciellogroup.com/blog/?p=604. Added 
+//         support for the "shortcut" format $(window).hashchange( fn ) and
+//         $(window).hashchange() like jQuery provides for built-in events.
+//         Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
+//         lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
+//         and <jQuery.fn.hashchange.src> properties plus document-domain.html
+//         file to address access denied issues when setting document.domain in
+//         IE6/7.
+// 1.2   - (2/11/2010) Fixed a bug where coming back to a page using this plugin
+//         from a page on another domain would cause an error in Safari 4. Also,
+//         IE6/7 Iframe is now inserted after the body (this actually works),
+//         which prevents the page from scrolling when the event is first bound.
+//         Event can also now be bound before DOM ready, but it won't be usable
+//         before then in IE6/7.
+// 1.1   - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
+//         where browser version is incorrectly reported as 8.0, despite
+//         inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
+// 1.0   - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
+//         window.onhashchange functionality into a separate plugin for users
+//         who want just the basic event & back button support, without all the
+//         extra awesomeness that BBQ provides. This plugin will be included as
+//         part of jQuery BBQ, but also be available separately.
+
+(function($,window,undefined){
+  '$:nomunge'; // Used by YUI compressor.
+  
+  // Reused string.
+  var str_hashchange = 'hashchange',
+    
+    // Method / object references.
+    doc = document,
+    fake_onhashchange,
+    special = $.event.special,
+    
+    // Does the browser support window.onhashchange? Note that IE8 running in
+    // IE7 compatibility mode reports true for 'onhashchange' in window, even
+    // though the event isn't supported, so also test document.documentMode.
+    doc_mode = doc.documentMode,
+    supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 );
+  
+  // Get location.hash (or what you'd expect location.hash to be) sans any
+  // leading #. Thanks for making this necessary, Firefox!
+  function get_fragment( url ) {
+    url = url || location.href;
+    return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
+  };
+  
+  // Method: jQuery.fn.hashchange
+  // 
+  // Bind a handler to the window.onhashchange event or trigger all bound
+  // window.onhashchange event handlers. This behavior is consistent with
+  // jQuery's built-in event handlers.
+  // 
+  // Usage:
+  // 
+  // > jQuery(window).hashchange( [ handler ] );
+  // 
+  // Arguments:
+  // 
+  //  handler - (Function) Optional handler to be bound to the hashchange
+  //    event. This is a "shortcut" for the more verbose form:
+  //    jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
+  //    all bound window.onhashchange event handlers will be triggered. This
+  //    is a shortcut for the more verbose
+  //    jQuery(window).trigger( 'hashchange' ). These forms are described in
+  //    the <hashchange event> section.
+  // 
+  // Returns:
+  // 
+  //  (jQuery) The initial jQuery collection of elements.
+  
+  // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
+  // $(elem).hashchange() for triggering, like jQuery does for built-in events.
+  $.fn[ str_hashchange ] = function( fn ) {
+    return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
+  };
+  
+  // Property: jQuery.fn.hashchange.delay
+  // 
+  // The numeric interval (in milliseconds) at which the <hashchange event>
+  // polling loop executes. Defaults to 50.
+  
+  // Property: jQuery.fn.hashchange.domain
+  // 
+  // If you're setting document.domain in your JavaScript, and you want hash
+  // history to work in IE6/7, not only must this property be set, but you must
+  // also set document.domain BEFORE jQuery is loaded into the page. This
+  // property is only applicable if you are supporting IE6/7 (or IE8 operating
+  // in "IE7 compatibility" mode).
+  // 
+  // In addition, the <jQuery.fn.hashchange.src> property must be set to the
+  // path of the included "document-domain.html" file, which can be renamed or
+  // modified if necessary (note that the document.domain specified must be the
+  // same in both your main JavaScript as well as in this file).
+  // 
+  // Usage:
+  // 
+  // jQuery.fn.hashchange.domain = document.domain;
+  
+  // Property: jQuery.fn.hashchange.src
+  // 
+  // If, for some reason, you need to specify an Iframe src file (for example,
+  // when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
+  // do so using this property. Note that when using this property, history
+  // won't be recorded in IE6/7 until the Iframe src file loads. This property
+  // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
+  // compatibility" mode).
+  // 
+  // Usage:
+  // 
+  // jQuery.fn.hashchange.src = 'path/to/file.html';
+  
+  $.fn[ str_hashchange ].delay = 50;
+  /*
+  $.fn[ str_hashchange ].domain = null;
+  $.fn[ str_hashchange ].src = null;
+  */
+  
+  // Event: hashchange event
+  // 
+  // Fired when location.hash changes. In browsers that support it, the native
+  // HTML5 window.onhashchange event is used, otherwise a polling loop is
+  // initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
+  // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
+  // compatibility" mode), a hidden Iframe is created to allow the back button
+  // and hash-based history to work.
+  // 
+  // Usage as described in <jQuery.fn.hashchange>:
+  // 
+  // > // Bind an event handler.
+  // > jQuery(window).hashchange( function(e) {
+  // >   var hash = location.hash;
+  // >   ...
+  // > });
+  // > 
+  // > // Manually trigger the event handler.
+  // > jQuery(window).hashchange();
+  // 
+  // A more verbose usage that allows for event namespacing:
+  // 
+  // > // Bind an event handler.
+  // > jQuery(window).bind( 'hashchange', function(e) {
+  // >   var hash = location.hash;
+  // >   ...
+  // > });
+  // > 
+  // > // Manually trigger the event handler.
+  // > jQuery(window).trigger( 'hashchange' );
+  // 
+  // Additional Notes:
+  // 
+  // * The polling loop and Iframe are not created until at least one handler
+  //   is actually bound to the 'hashchange' event.
+  // * If you need the bound handler(s) to execute immediately, in cases where
+  //   a location.hash exists on page load, via bookmark or page refresh for
+  //   example, use jQuery(window).hashchange() or the more verbose 
+  //   jQuery(window).trigger( 'hashchange' ).
+  // * The event can be bound before DOM ready, but since it won't be usable
+  //   before then in IE6/7 (due to the necessary Iframe), recommended usage is
+  //   to bind it inside a DOM ready handler.
+  
+  // Override existing $.event.special.hashchange methods (allowing this plugin
+  // to be defined after jQuery BBQ in BBQ's source code).
+  special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
+    
+    // Called only when the first 'hashchange' event is bound to window.
+    setup: function() {
+      // If window.onhashchange is supported natively, there's nothing to do..
+      if ( supports_onhashchange ) { return false; }
+      
+      // Otherwise, we need to create our own. And we don't want to call this
+      // until the user binds to the event, just in case they never do, since it
+      // will create a polling loop and possibly even a hidden Iframe.
+      $( fake_onhashchange.start );
+    },
+    
+    // Called only when the last 'hashchange' event is unbound from window.
+    teardown: function() {
+      // If window.onhashchange is supported natively, there's nothing to do..
+      if ( supports_onhashchange ) { return false; }
+      
+      // Otherwise, we need to stop ours (if possible).
+      $( fake_onhashchange.stop );
+    }
+    
+  });
+  
+  // fake_onhashchange does all the work of triggering the window.onhashchange
+  // event for browsers that don't natively support it, including creating a
+  // polling loop to watch for hash changes and in IE 6/7 creating a hidden
+  // Iframe to enable back and forward.
+  fake_onhashchange = (function(){
+    var self = {},
+      timeout_id,
+      
+      // Remember the initial hash so it doesn't get triggered immediately.
+      last_hash = get_fragment(),
+      
+      fn_retval = function(val){ return val; },
+      history_set = fn_retval,
+      history_get = fn_retval;
+    
+    // Start the polling loop.
+    self.start = function() {
+      timeout_id || poll();
+    };
+    
+    // Stop the polling loop.
+    self.stop = function() {
+      timeout_id && clearTimeout( timeout_id );
+      timeout_id = undefined;
+    };
+    
+    // This polling loop checks every $.fn.hashchange.delay milliseconds to see
+    // if location.hash has changed, and triggers the 'hashchange' event on
+    // window when necessary.
+    function poll() {
+      var hash = get_fragment(),
+        history_hash = history_get( last_hash );
+      
+      if ( hash !== last_hash ) {
+        history_set( last_hash = hash, history_hash );
+        
+        $(window).trigger( str_hashchange );
+        
+      } else if ( history_hash !== last_hash ) {
+        location.href = location.href.replace( /#.*/, '' ) + history_hash;
+      }
+      
+      timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
+    };
+    
+    // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+    // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
+    // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+    $.browser.msie && !supports_onhashchange && (function(){
+      // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
+      // when running in "IE7 compatibility" mode.
+      
+      var iframe,
+        iframe_src;
+      
+      // When the event is bound and polling starts in IE 6/7, create a hidden
+      // Iframe for history handling.
+      self.start = function(){
+        if ( !iframe ) {
+          iframe_src = $.fn[ str_hashchange ].src;
+          iframe_src = iframe_src && iframe_src + get_fragment();
+          
+          // Create hidden Iframe. Attempt to make Iframe as hidden as possible
+          // by using techniques from http://www.paciellogroup.com/blog/?p=604.
+          iframe = $('<iframe tabindex="-1" title="empty"/>').hide()
+            
+            // When Iframe has completely loaded, initialize the history and
+            // start polling.
+            .one( 'load', function(){
+              iframe_src || history_set( get_fragment() );
+              poll();
+            })
+            
+            // Load Iframe src if specified, otherwise nothing.
+            .attr( 'src', iframe_src || 'javascript:0' )
+            
+            // Append Iframe after the end of the body to prevent unnecessary
+            // initial page scrolling (yes, this works).
+            .insertAfter( 'body' )[0].contentWindow;
+          
+          // Whenever `document.title` changes, update the Iframe's title to
+          // prettify the back/next history menu entries. Since IE sometimes
+          // errors with "Unspecified error" the very first time this is set
+          // (yes, very useful) wrap this with a try/catch block.
+          doc.onpropertychange = function(){
+            try {
+              if ( event.propertyName === 'title' ) {
+                iframe.document.title = doc.title;
+              }
+            } catch(e) {}
+          };
+          
+        }
+      };
+      
+      // Override the "stop" method since an IE6/7 Iframe was created. Even
+      // if there are no longer any bound event handlers, the polling loop
+      // is still necessary for back/next to work at all!
+      self.stop = fn_retval;
+      
+      // Get history by looking at the hidden Iframe's location.hash.
+      history_get = function() {
+        return get_fragment( iframe.location.href );
+      };
+      
+      // Set a new history item by opening and then closing the Iframe
+      // document, *then* setting its location.hash. If document.domain has
+      // been set, update that as well.
+      history_set = function( hash, history_hash ) {
+        var iframe_doc = iframe.document,
+          domain = $.fn[ str_hashchange ].domain;
+        
+        if ( hash !== history_hash ) {
+          // Update Iframe with any initial `document.title` that might be set.
+          iframe_doc.title = doc.title;
+          
+          // Opening the Iframe's document after it has been closed is what
+          // actually adds a history entry.
+          iframe_doc.open();
+          
+          // Set document.domain for the Iframe document as well, if necessary.
+          domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' );
+          
+          iframe_doc.close();
+          
+          // Update the Iframe's hash, for great justice.
+          iframe.location.hash = hash;
+        }
+      };
+      
+    })();
+    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    // ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
+    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    
+    return self;
+  })();
+  
+})(jQuery,this);
diff --git a/sweettooth/static/js/main.js b/sweettooth/static/js/main.js
index 2f96ad9..f9b0869 100644
--- a/sweettooth/static/js/main.js
+++ b/sweettooth/static/js/main.js
@@ -1,6 +1,6 @@
 "use strict";
 
-require(['jquery', 'messages', 'extensions',
+require(['jquery', 'messages', 'extensions', 'paginator',
          'jquery.cookie', 'jquery.jeditable',
          'jquery.timeago', 'jquery.rating'], function($, messages) {
     if (!$.ajaxSettings.headers)
@@ -103,6 +103,9 @@ require(['jquery', 'messages', 'extensions',
 
         $('#extension_shell_versions_info').buildShellVersionsInfo();
 
+        var $extList = $('#extensions-list');
+        $extList.paginatorify('/ajax/extensions-list/');
+
         $('.extension_status_toggle a').click(function() {
             var $link = $(this);
             var $tr = $link.parents('tr');
diff --git a/sweettooth/static/js/paginator.js b/sweettooth/static/js/paginator.js
new file mode 100644
index 0000000..2dddb12
--- /dev/null
+++ b/sweettooth/static/js/paginator.js
@@ -0,0 +1,149 @@
+"use strict"
+
+define(['jquery', 'jquery.hashchange'], function($) {
+
+    function getHashParams() {
+        var hash = window.location.hash;
+        if (!hash)
+            return {};
+
+        var values = hash.slice(1).split('&');
+        var obj = {};
+        for (var i = 0; i < values.length; i++) {
+            if (!values[i])
+                continue;
+
+            var kv = values[i].split('=');
+            obj[kv[0]] = kv[1];
+        }
+
+        return obj;
+    }
+
+    function makeHashParams(obj) {
+        var hash = '';
+        for (var key in obj) {
+            hash += key + '=' + obj[key] + '&';
+        }
+
+        // Remove last '&'
+        return hash.slice(0, -1);
+    }
+
+    function setHashParams(obj) {
+        window.location.hash = makeHashParams(obj);
+    }
+
+    $.fn.paginatorify = function(url, context) {
+        var hashParams = {};
+
+        if (context === undefined)
+            context = 3;
+
+        var $elem = $(this);
+        var numPages = 0;
+        var $beforePaginator = null;
+        var $afterPaginator = null;
+
+        function loadPage() {
+            $elem.addClass('loading');
+
+            $.ajax({
+                url: url,
+                dataType: 'json',
+                data: hashParams,
+                type: 'GET'
+            }).done(function(result) {
+                if ($beforePaginator)
+                    $beforePaginator.detach();
+                if ($afterPaginator)
+                $afterPaginator.detach();
+
+                numPages = result.numpages;
+
+                $beforePaginator = buildPaginator();
+                $afterPaginator = buildPaginator();
+
+                var $newContent = $(result.html);
+
+                $elem.
+                    removeClass('loading').
+                    empty().
+                    append($beforePaginator).
+                    append($newContent).
+                    append($afterPaginator);
+            });
+        }
+
+        function makeLink(pageNumber, styleClass, text) {
+            styleClass = styleClass === undefined ? "" : styleClass;
+            text = text === undefined ? pageNumber.toString() : text;
+
+            var hp = $.extend({}, hashParams);
+            hp.page = pageNumber;
+
+            return $('<a>', {'class': 'number ' + styleClass,
+                             'href': '#' + makeHashParams(hp)}).text(text);
+        }
+
+        function buildPaginator() {
+            var number = hashParams.page;
+            var contextLeft = Math.max(number-context, 2);
+            var contextRight = Math.min(number+context+2, numPages);
+
+            var $elem = $('<div>', {'class': 'paginator'});
+
+            if (number > 1) {
+                makeLink(number-1, 'prev', "Previous").appendTo($elem);
+                makeLink(1, 'first').appendTo($elem);
+                if (number-context > 2)
+                    $elem.append($('<span>', {'class': 'ellipses'}).text("..."));
+
+                for (var i = contextLeft; i < number; i++)
+                    makeLink(i).appendTo($elem);
+            }
+
+            $elem.append($('<span>', {'class': 'current number'}).text(number));
+
+            if (number < numPages) {
+                for (var i = number+1; i < contextRight; i++)
+                    makeLink(i).appendTo($elem);
+
+                if (numPages - (number+context) > 1)
+                    $elem.append($('<span>', {'class': 'ellipses'}).text("..."));
+
+                makeLink(numPages, 'last').appendTo($elem);
+                makeLink(number+1, 'prev', "Next").appendTo($elem);
+            }
+
+            return $elem;
+        }
+
+        function hashChanged(hp) {
+            if (hashParams.sort !== hp.sort)
+                return true;
+
+            if (hashParams.page !== hp.sort)
+                return true;
+
+            return false;
+        }
+
+        $(window).hashchange(function() {
+            var hp = getHashParams();
+            if (hashChanged) {
+                hashParams = hp;
+
+                if (hashParams.page === undefined)
+                    hashParams.page = 1;
+                else
+                    hashParams.page = parseInt(hashParams.page);
+
+                loadPage();
+            }
+        });
+
+        $(window).hashchange();
+    };
+
+});



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]