ajax search results: a cvan/gkobes collabo (bug 684342)

This commit is contained in:
Gregory Koberger 2011-10-12 16:29:38 -07:00 коммит произвёл Chris Van
Родитель a9564b1c04
Коммит e07256c6e6
17 изменённых файлов: 307 добавлений и 120 удалений

Просмотреть файл

@ -1,4 +1,4 @@
<div id="sorter" class="c">
<div id="sorter" class="c pjax-trigger">
<h3>{{ _('Sort by:') }}</h3>
<ul>
{% for item in sort_opts %}

Просмотреть файл

@ -1,5 +1,5 @@
{% if pager.paginator.num_pages > 1 %}
<nav class="paginator c">
<nav class="paginator c pjax-trigger">
<p class="num">
{# L10n: This is a page range (e.g., Page 1 of 50). #}
{% trans current_pg=pager.number,
@ -16,10 +16,10 @@
{% if not pager.has_previous() %}class="disabled"{% endif %}>
&#x25C2;&#x25C2;</a>
<a href="{{ pager.url|urlparams(page=pager.previous_page_number()) }}"
class="button{% if not pager.has_previous() %} disabled{% endif %}">
class="button prev{% if not pager.has_previous() %} disabled{% endif %}">
&#x25C2; {{ _('Previous') }}</a>
<a href="{{ pager.url|urlparams(page=pager.next_page_number()) }}"
class="button{% if not pager.has_next() %} disabled{% endif %}">
class="button next{% if not pager.has_next() %} disabled{% endif %}">
{{ _('Next') }} &#x25B8;</a>
<a href="{{ pager.url|urlparams(page=pager.paginator.num_pages) }}"
title="{{ _('Jump to last page') }}"

Просмотреть файл

@ -1,4 +1,8 @@
{% extends "impala/base.html" %}
{% extends "base_ajax.html" if is_pjax else "impala/base.html" %}
{% block bodyclass %}
{{ 'pjax' if waffle.switch('ajax-search') }} {{ super() }}
{% endblock %}
{% block search_form %}
{% with skip_autofill=True %}
@ -47,9 +51,12 @@
{% endmacro %}
{% block content %}
<section id="search-facets" class="secondary" role="complementary">
{% if is_pjax %}
{% include "search/results_inner.html" %}
{% else %}
<section id="search-facets" class="secondary" role="complementary">
<h2>{{ _('Filter Results') }}</h2>
<ul class="facets island">
<ul class="facets island pjax-trigger">
{{ facet(_('Category'), categories) }}
<li class="facet">
<h3>{{ _('Works with') }}</h3>
@ -63,26 +70,15 @@
{{ facet(_('Tag'), tags) }}
</ul>
<p>{{ _('{0} matching results')|f(pager.paginator.count|numberfmt) }}</p>
</section>
</section>
<section class="primary" role="main">
<div class="listing results island hero c">
<section class="primary" role="main">
<h1>{{ heading }}</h1>
{{ impala_addon_listing_header(
request.get_full_path()|urlparams(page=None),
sort_opts, query.sort, extra_sort_opts) }}
{% if pager.object_list %}
<div class="items">
{{ impala_addon_listing_items(pager.object_list, field=query.sort,
src='search') }}
<div class="listing results island hero c">
<div id="pjax-results">
{% include "search/results_inner.html" %}
</div>
{{ pager|impala_paginator }}
{% else %}
{% include 'search/no_results.html' %}
</div>
</section>
{% endif %}
</div>
</section>
{% endblock %}

Просмотреть файл

@ -0,0 +1,11 @@
{{ impala_addon_listing_header(request.get_full_path()|urlparams(page=None),
sort_opts, query.sort, extra_sort_opts) }}
{% if pager.object_list %}
<div class="items">
{{ impala_addon_listing_items(pager.object_list, field=query.sort,
src='search') }}
</div>
{{ pager|impala_paginator }}
{% else %}
{% include 'search/no_results.html' %}
{% endif %}

Просмотреть файл

@ -104,9 +104,10 @@ class TestESSearch(amo.tests.ESTestCase):
def setUpClass(cls):
super(TestESSearch, cls).setUpClass()
cls.setUpIndex()
cls.search_views = ('search.search', 'apps.search')
@amo.tests.mobile_test
def test_mobile_get(self):
def test_mobile_results(self):
r = self.client.get(reverse('search.search'))
eq_(r.status_code, 200)
self.assertTemplateUsed(r, 'search/mobile/results.html')
@ -116,6 +117,40 @@ class TestESSearch(amo.tests.ESTestCase):
r = self.client.get(base + '?sort=averagerating')
self.assertRedirects(r, base + '?sort=rating', status_code=301)
def test_results(self):
# These context variables should exist for normal requests.
expected_context_vars = {
'search.search': ('categories', 'platforms', 'versions', 'tags'),
'apps.search': ('categories', 'tags'),
}
for view in self.search_views:
r = self.client.get(reverse(view))
eq_(r.status_code, 200)
eq_(r.context['is_pjax'], None)
for var in expected_context_vars[view]:
assert var in r.context, (
'%r missing context var in view %r' % (var, view))
doc = pq(r.content)
eq_(doc('html').length, 1)
eq_(doc('#pjax-results').length, 1)
eq_(doc('#search-facets .facets.pjax-trigger').length, 1)
eq_(doc('#sorter.pjax-trigger').length, 1)
def test_pjax_results(self):
for view in self.search_views:
r = self.client.get(reverse(view), HTTP_X_PJAX=True)
eq_(r.status_code, 200)
eq_(r.context['is_pjax'], True)
doc = pq(r.content)
eq_(doc('html').length, 0)
eq_(doc('#pjax-results').length, 0)
eq_(doc('#search-facets .facets.pjax-trigger').length, 0)
eq_(doc('#sorter.pjax-trigger').length, 1)
def assert_ajax_query(self, params, addons=[]):
r = self.client.get(reverse('search.ajax') + '?' + params)
eq_(r.status_code, 200)
@ -172,7 +207,6 @@ class TestESSearch(amo.tests.ESTestCase):
addon.update(type=amo.ADDON_PERSONA)
Persona.objects.create(persona_id=4, addon_id=4)
self.assert_ajax_query('q=4', [addon])
self.assert_ajax_query('q=4&exclude_personas=true', [])
def test_ajax_search_char_limit(self):
self.assert_ajax_query('q=ad', [])

Просмотреть файл

@ -418,14 +418,18 @@ def app_search(request, template=None):
facets = pager.object_list.facets
ctx = {
'is_pjax': request.META.get('HTTP_X_PJAX'),
'pager': pager,
'query': query,
'form': form,
'sorting': sort_sidebar(request, query, form),
'sort_opts': form.fields['sort'].choices,
}
if not ctx['is_pjax']:
ctx.update({
'categories': category_sidebar(request, query, facets),
'tags': tag_sidebar(request, query, facets),
}
})
return jingo.render(request, template, ctx)
@ -500,20 +504,24 @@ def search(request, tag_name=None, template=None):
qs = qs.order_by('-weekly_downloads')
pager = amo.utils.paginate(request, qs)
facets = pager.object_list.facets
ctx = {
'is_pjax': request.META.get('HTTP_X_PJAX'),
'pager': pager,
'query': query,
'form': form,
'sort_opts': sort,
'extra_sort_opts': extra_sort,
'sorting': sort_sidebar(request, query, form),
}
if not ctx['is_pjax']:
facets = pager.object_list.facets
ctx.update({
'categories': category_sidebar(request, query, facets),
'platforms': platform_sidebar(request, query, facets),
'versions': version_sidebar(request, query, facets),
'tags': tag_sidebar(request, query, facets),
}
})
return jingo.render(request, template, ctx)

Просмотреть файл

@ -123,5 +123,49 @@
li ul {
padding: 0 12px 0 0;
}
.facets .facet {
&:after {
right: auto;
left: 5px;
}
&.active:after {
-moz-transform: rotate(-90deg);
}
}
}
}
.results {
position: relative;
&.loading {
.updating {
background: rgba(255,255,255, 0.8)
url(../../img/impala/loading-big.gif)
50% 50px no-repeat;
border: 1px solid #ddd;
.box-shadow(0 -2px 0 rgba(200, 200, 200, 0.3) inset,
0 0 1px rgba(0, 0, 0, 0.1));
.border-box;
.border-radius(5px);
color: @medium-gray;
font: bold 20px @head-sans;
margin-left: -250px / 2;
position: absolute;
top: 45px;
left: 50%;
padding: 15px 15px 45px;
text-align: center;
z-index: 100;
width: 250px;
&.tall {
top: 200px;
}
}
.items {
opacity: .2;
}
}
#sorter {
float: none;
}
}

Двоичные данные
media/img/impala/loading-big.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 11 KiB

Просмотреть файл

@ -13,3 +13,9 @@ function populateErrors(context, o) {
$row.append($list.append($(format('<li>{0}</li>', v))));
});
}
function fieldFocused(e) {
var tags = /input|keygen|meter|option|output|progress|select|textarea/i;
return tags.test(e.target.nodeName);
}

Просмотреть файл

@ -5,16 +5,7 @@ $(function() {
$p.popup($a, {width: 300, pointTo: $a});
});
// Mark incompatible add-ons on listing pages unless marked with ignore.
$('.listing .item.addon').each(function() {
var $this = $(this);
if ($this.find('.acr-override').length) {
$this.addClass('acr');
} else if (!$this.hasClass('ignore-compatibility') &&
$this.find('.concealed').length == $this.find('.button').length) {
$this.addClass('incompatible');
}
});
initListingCompat();
$('.theme-grid .hovercard.theme').each(function() {
var $this = $(this);
@ -38,3 +29,18 @@ $(function() {
$('.item.static').removeClass('static');
});
});
function initListingCompat(domContext) {
domContext = domContext || document.body;
// Mark incompatible add-ons on listing pages unless marked with ignore.
$('.listing .item.addon', domContext).each(function() {
var $this = $(this);
if ($this.find('.acr-override').length) {
$this.addClass('acr');
} else if (!$this.hasClass('ignore-compatibility') &&
$this.find('.concealed').length == $this.find('.button').length) {
$this.addClass('incompatible');
}
});
}

Просмотреть файл

@ -1,13 +1,91 @@
$(function() {
$('#search-facets').delegate('li.facet', 'click', function() {
$('#search-facets').delegate('li.facet', 'click', function(e) {
var $this = $(this);
if ($this.hasClass('active')) {
var $tgt = $(e.target);
if ($tgt.is('a')) {
$tgt.closest('.facet').find('.selected').removeClass('selected');
$tgt.closest('li').addClass('selected');
return;
}
$this.removeClass('active');
} else {
$this.closest('ul').find('.active').removeClass('active');
$this.addClass('active');
}
}).delegate('li.facet a', 'click', function(e) {
e.stopPropagation();
});
if ($('body').hasClass('pjax') && $.support.pjax) {
initSearchPjax('#pjax-results');
}
});
function initSearchPjax(container) {
var $container = $(container);
function pjaxOpen(url) {
var urlBase = location.pathname + location.search;
if (!!url && url != '#' && url != urlBase) {
$.pjax({'url': url, 'container': container});
}
}
function hijackLink() {
pjaxOpen($(this).attr('href'));
}
function loading() {
var $this = $(this),
$wrapper = $this.closest('.results'),
msg = gettext('Updating results&hellip;'),
cls = 'updating';
$wrapper.addClass('loading');
// The loading indicator is absolutely positioned atop the
// search results, so we do this to ensure a max-margin of sorts.
if ($this.outerHeight() > 300) {
cls += ' tall';
}
// Insert the loading indicator.
$('<div>', {'class': cls, 'html': msg}).insertBefore($this);
}
function finished() {
var $this = $(this),
$wrapper = $this.closest('.results');
// Initialize install buttons and compatibility checking.
$.when($this.find('.install:not(.triggered)').installButton()).done(function() {
$this.find('.install').addClass('triggered');
initListingCompat();
});
// Remove the loading indicator.
$wrapper.removeClass('loading').find('.updating').remove();
// Scroll up.
$('html').animate({scrollTop: 0}, 200);
}
function turnPages(e) {
if (fieldFocused(e)) {
return;
}
if (e.which == $.ui.keyCode.LEFT || e.which == $.ui.keyCode.RIGHT) {
e.preventDefault();
var sel;
if (e.which == $.ui.keyCode.LEFT) {
sel = '.paginator .prev:not(.disabled)';
} else {
sel = '.paginator .next:not(.disabled)';
}
pjaxOpen($container.find(sel).attr('href'));
}
}
$('.pjax-trigger a').live('click', _pd(hijackLink));
$container.bind('start.pjax', loading).bind('end.pjax', finished);
$(document).keyup(_.throttle(turnPages, 300));
}

Просмотреть файл

@ -163,8 +163,7 @@ $.fn.searchSuggestions = function(results) {
});
$(document).keyup(function(e) {
if (/input|keygen|meter|option|output|progress|select|textarea/i.test(e.target.nodeName) ||
e.target.type === 'text') {
if (fieldFocused(e)) {
return;
}
if (e.which == 16 || e.which == 83) {

Просмотреть файл

@ -25,15 +25,15 @@
// Returns the jQuery object
$.fn.pjax = function( container, options ) {
if ( options )
options.container = container
options.container = container;
else
options = $.isPlainObject(container) ? container : {container:container}
options = $.isPlainObject(container) ? container : {container:container};
// We can't persist $objects using the history API so we must use
// a String selector. Bail if we got anything else.
if ( options.container && typeof options.container !== 'string' ) {
throw "pjax container must be a string selector!"
return false
throw "pjax container must be a string selector!";
return false;
}
return this.live('click', function(event){
@ -42,18 +42,18 @@ $.fn.pjax = function( container, options ) {
if ( event.which > 1 || event.metaKey ||
// the href was only a hash:
this.href.replace(this.hash || '', '') == window.location)
return true
return true;
var defaults = {
url: this.href,
container: $(this).attr('data-pjax'),
clickedElement: $(this),
fragment: null
}
};
$.pjax($.extend({}, defaults, options))
$.pjax($.extend({}, defaults, options));
event.preventDefault()
event.preventDefault();
})
}
@ -79,100 +79,100 @@ $.fn.pjax = function( container, options ) {
// Returns whatever $.ajax returns.
var pjax = $.pjax = function( options ) {
var $container = $(options.container),
success = options.success || $.noop
success = options.success || $.noop;
// We don't want to let anyone override our success handler.
delete options.success
delete options.success;
// We can't persist $objects using the history API so we must use
// a String selector. Bail if we got anything else.
if ( typeof options.container !== 'string' )
throw "pjax container must be a string selector!"
throw "pjax container must be a string selector!";
options = $.extend(true, {}, pjax.defaults, options)
if ( $.isFunction(options.url) ) {
options.url = options.url()
options.url = options.url();
}
options.context = $container
options.context = $container;
options.success = function(data){
if ( options.fragment ) {
// If they specified a fragment, look for it in the response
// and pull it out.
var $fragment = $(data).find(options.fragment)
var $fragment = $(data).find(options.fragment);
if ( $fragment.length )
data = $fragment.children()
data = $fragment.children();
else
return window.location = options.url
return window.location = options.url;
} else {
// If we got no data or an entire web page, go directly
// to the page and let normal error handling happen.
if ( !$.trim(data) || /<html/i.test(data) )
return window.location = options.url
return window.location = options.url;
}
// Make it happen.
this.html(data)
this.html(data);
// If there's a <title> tag in the response, use it as
// the page's title.
var oldTitle = document.title,
title = $.trim( this.find('title').remove().text() )
if ( title ) document.title = title
title = $.trim( this.find('title').remove().text() );
if ( title ) document.title = title;
var state = {
pjax: options.container,
fragment: options.fragment,
timeout: options.timeout
}
};
// If there are extra params, save the complete URL in the state object
var query = $.param(options.data)
var query = $.param(options.data);
if ( query != "_pjax=true" )
state.url = options.url + (/\?/.test(options.url) ? "&" : "?") + query
state.url = options.url + (/\?/.test(options.url) ? "&" : "?") + query;
if ( options.replace ) {
window.history.replaceState(state, document.title, options.url)
window.history.replaceState(state, document.title, options.url);
} else if ( options.push ) {
// this extra replaceState before first push ensures good back
// button behavior
if ( !pjax.active ) {
window.history.replaceState($.extend({}, state, {url:null}), oldTitle)
pjax.active = true
window.history.replaceState($.extend({}, state, {url:null}), oldTitle);
pjax.active = true;
}
window.history.pushState(state, document.title, options.url)
window.history.pushState(state, document.title, options.url);
}
// Google Analytics support
if ( (options.replace || options.push) && window._gaq )
_gaq.push(['_trackPageview'])
_gaq.push(['_trackPageview']);
// If the URL has a hash in it, make sure the browser
// knows to navigate to the hash.
var hash = window.location.hash.toString()
var hash = window.location.hash.toString();
if ( hash !== '' ) {
window.location.href = hash
window.location.href = hash;
}
// Invoke their success handler if they gave us one.
success.apply(this, arguments)
success.apply(this, arguments);
}
// Cancel the current request if we're already pjaxing
var xhr = pjax.xhr
var xhr = pjax.xhr;
if ( xhr && xhr.readyState < 4) {
xhr.onreadystatechange = $.noop
xhr.abort()
xhr.onreadystatechange = $.noop;
xhr.abort();
}
pjax.options = options
pjax.xhr = $.ajax(options)
$(document).trigger('pjax', [pjax.xhr, options])
pjax.options = options;
pjax.xhr = $.ajax(options);
$(document).trigger('pjax', [pjax.xhr, options]);
return pjax.xhr
return pjax.xhr;
}
@ -187,22 +187,22 @@ pjax.defaults = {
type: 'GET',
dataType: 'html',
beforeSend: function(xhr){
this.trigger('start.pjax', [xhr, pjax.options])
xhr.setRequestHeader('X-PJAX', 'true')
this.trigger('start.pjax', [xhr, pjax.options]);
xhr.setRequestHeader('X-PJAX', 'true');
},
error: function(xhr, textStatus, errorThrown){
if ( textStatus !== 'abort' )
window.location = pjax.options.url
window.location = pjax.options.url;
},
complete: function(xhr){
this.trigger('end.pjax', [xhr, pjax.options])
this.trigger('end.pjax', [xhr, pjax.options]);
}
}
// Used to detect initial (useless) popstate.
// If history.state exists, assume browser isn't going to fire initial popstate.
var popped = ('state' in window.history), initialURL = location.href
var popped = ('state' in window.history), initialURL = location.href;
// popstate handler takes care of the back and forward buttons
@ -211,14 +211,14 @@ var popped = ('state' in window.history), initialURL = location.href
// stuff yet.
$(window).bind('popstate', function(event){
// Ignore inital popstate that some browsers fire on page load
var initialPop = !popped && location.href == initialURL
popped = true
if ( initialPop ) return
var initialPop = !popped && location.href == initialURL;
popped = true;
if ( initialPop ) return;
var state = event.state
var state = event.state;
if ( state && state.pjax ) {
var container = state.pjax
var container = state.pjax;
if ( $(container+'').length )
$.pjax({
url: state.url || location.href,
@ -226,9 +226,9 @@ $(window).bind('popstate', function(event){
container: container,
push: false,
timeout: state.timeout
})
});
else
window.location = location.href
window.location = location.href;
}
})
@ -236,22 +236,22 @@ $(window).bind('popstate', function(event){
// Add the state property to jQuery's event object so we can use it in
// $(window).bind('popstate')
if ( $.inArray('state', $.event.props) < 0 )
$.event.props.push('state')
$.event.props.push('state');
// Is pjax supported by this browser?
$.support.pjax =
window.history && window.history.pushState && window.history.replaceState
// pushState isn't reliable on iOS yet.
&& !navigator.userAgent.match(/(iPod|iPhone|iPad|WebApps\/.+CFNetwork)/)
&& !navigator.userAgent.match(/(iPod|iPhone|iPad|WebApps\/.+CFNetwork)/);
// Fall back to normalcy for older browsers.
if ( !$.support.pjax ) {
$.pjax = function( options ) {
window.location = $.isFunction(options.url) ? options.url() : options.url
}
$.fn.pjax = function() { return this }
};
$.fn.pjax = function() { return this };
}
})(jQuery);

Просмотреть файл

@ -138,7 +138,9 @@ var installButton = function() {
}
};
var addWarning = function(msg, type) { $this.parent().append(format(type || notavail, [msg])); };
var addWarning = function(msg, type) {
$this.parent().append(format(type || notavail, [msg]));
};
// Change the button text to "Add to Firefox".
var addToApp = function() {

Просмотреть файл

@ -0,0 +1 @@
INSERT INTO waffle_switch (name, active) VALUES ('ajax-search', 0);

Просмотреть файл

@ -590,6 +590,7 @@ MINIFY_BUNDLES = {
'js/lib/jquery.cookie.js',
'js/zamboni/storage.js',
'js/zamboni/buttons.js',
'js/lib/jquery.pjax.js',
# jQuery UI
'js/lib/jquery-ui/jquery.ui.core.js',

1
templates/base_ajax.html Normal file
Просмотреть файл

@ -0,0 +1 @@
{% block content %}{% endblock %}