implement search suggestions (bug 546826)
This commit is contained in:
Родитель
6238485813
Коммит
f647f49f5d
|
@ -6,5 +6,7 @@ from . import views
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url('^$', views.search, name='search.search'),
|
url('^$', views.search, name='search.search'),
|
||||||
url('^ajax$', views.ajax_search, name='search.ajax'),
|
url('^ajax$', views.ajax_search, name='search.ajax'),
|
||||||
|
url('^suggestions$', views.ajax_search_suggestions,
|
||||||
|
name='search.suggestions'),
|
||||||
url('^es$', views.es_search, name='search.es_search'),
|
url('^es$', views.es_search, name='search.es_search'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,7 @@ import bandwagon.views
|
||||||
import browse.views
|
import browse.views
|
||||||
from addons.models import Addon, Category
|
from addons.models import Addon, Category
|
||||||
from amo.decorators import json_view
|
from amo.decorators import json_view
|
||||||
from amo.helpers import urlparams
|
from amo.helpers import locale_url, urlparams
|
||||||
from amo.urlresolvers import reverse
|
from amo.urlresolvers import reverse
|
||||||
from amo.utils import MenuItem, sorted_groupby
|
from amo.utils import MenuItem, sorted_groupby
|
||||||
from versions.compare import dict_from_int, version_int
|
from versions.compare import dict_from_int, version_int
|
||||||
|
@ -318,6 +318,12 @@ class BaseAjaxSearch(object):
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionsAjax(BaseAjaxSearch):
|
||||||
|
# No personas. No webapps.
|
||||||
|
types = [amo.ADDON_ANY, amo.ADDON_EXTENSION, amo.ADDON_THEME,
|
||||||
|
amo.ADDON_DICT, amo.ADDON_SEARCH, amo.ADDON_LPAPP]
|
||||||
|
|
||||||
|
|
||||||
@json_view
|
@json_view
|
||||||
def ajax_search(request):
|
def ajax_search(request):
|
||||||
"""This is currently used only to return add-ons for populating a
|
"""This is currently used only to return add-ons for populating a
|
||||||
|
@ -328,6 +334,48 @@ def ajax_search(request):
|
||||||
return BaseAjaxSearch(request).items
|
return BaseAjaxSearch(request).items
|
||||||
|
|
||||||
|
|
||||||
|
@json_view
|
||||||
|
def ajax_search_suggestions(request):
|
||||||
|
# TODO(cvan): Tests will come when I know this is what fligtar wants.
|
||||||
|
results = []
|
||||||
|
q = request.GET.get('q')
|
||||||
|
if q and (q.isdigit() or (not q.isdigit() and len(q) > 2)):
|
||||||
|
q_ = q.lower()
|
||||||
|
|
||||||
|
# Applications.
|
||||||
|
for a in amo.APP_USAGE:
|
||||||
|
if q_ in unicode(a.pretty).lower():
|
||||||
|
results.append({
|
||||||
|
'id': a.id,
|
||||||
|
'label': _(u'{0} Add-ons').format(a.pretty),
|
||||||
|
'url': locale_url(a.short),
|
||||||
|
'cls': 'app ' + a.short
|
||||||
|
})
|
||||||
|
|
||||||
|
# Categories.
|
||||||
|
cats = (Category.objects
|
||||||
|
.filter(Q(application=request.APP.id) |
|
||||||
|
Q(type=amo.ADDON_SEARCH))
|
||||||
|
.exclude(type=amo.ADDON_WEBAPP))
|
||||||
|
for c in cats:
|
||||||
|
if not c.name:
|
||||||
|
continue
|
||||||
|
name_ = unicode(c.name).lower()
|
||||||
|
word_matches = [w for w in q_.split() if name_ in w]
|
||||||
|
if q_ in name_ or word_matches:
|
||||||
|
results.append({
|
||||||
|
'id': c.id,
|
||||||
|
'label': unicode(c.name),
|
||||||
|
'url': c.get_url_path(),
|
||||||
|
'cls': 'cat'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add-ons.
|
||||||
|
results += SuggestionsAjax(request).items
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def name_only_query(q):
|
def name_only_query(q):
|
||||||
return dict(name__text={'query': q, 'boost': 3, 'analyzer': 'standard'},
|
return dict(name__text={'query': q, 'boost': 3, 'analyzer': 'standard'},
|
||||||
name__fuzzy={'value': q, 'boost': 2, 'prefix_length': 4},
|
name__fuzzy={'value': q, 'boost': 2, 'prefix_length': 4},
|
||||||
|
|
|
@ -41,6 +41,18 @@
|
||||||
transition: @property @duration;
|
transition: @property @duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transition-property(@property) {
|
||||||
|
-moz-transition-property: @property;
|
||||||
|
-webkit-transition-property: @property;
|
||||||
|
transition-property: @property;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-duration(@duration:2s) {
|
||||||
|
-moz-transition-duration: @duration;
|
||||||
|
-webkit-transition-duration: @duration;
|
||||||
|
transition-duration: @duration;
|
||||||
|
}
|
||||||
|
|
||||||
.background-size(@size) {
|
.background-size(@size) {
|
||||||
-moz-background-size: @size;
|
-moz-background-size: @size;
|
||||||
-wekbkit-background-size: @size;
|
-wekbkit-background-size: @size;
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
@import 'lib';
|
||||||
|
|
||||||
|
#site-search-suggestions {
|
||||||
|
.transition-property(~'bottom, opacity, visibility');
|
||||||
|
.transition-duration(.3s);
|
||||||
|
font-size: 14px;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: -10px;
|
||||||
|
bottom: -5px;
|
||||||
|
left: -10px;
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: 1000;
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
bottom: -15px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #98B2C9;
|
||||||
|
.border-radius(4px);
|
||||||
|
.box-shadow(0 0 4px rgba(0, 0, 0, 0.2),
|
||||||
|
0 -2px 0 rgba(152, 178, 201, 0.3) inset);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
&:before {
|
||||||
|
background: url(../../img/impala/search-stem.png) 50% 100% no-repeat;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
left: 14px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0 16px 10px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 10px 16px 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
background: no-repeat 3px 3px;
|
||||||
|
.background-size(20px auto);
|
||||||
|
display: block;
|
||||||
|
font: 13px/26px @sans-stack;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 4px 0 32px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
&.cat {
|
||||||
|
background-image: url(../../img/icons/search-cat.png);
|
||||||
|
}
|
||||||
|
&.app {
|
||||||
|
background-image: url(../../img/app-icons/16/sprite.png);
|
||||||
|
.background-size(18px auto);
|
||||||
|
background-position: 4px 5px;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
&.thunderbird {
|
||||||
|
background-position: 5px -32px;
|
||||||
|
}
|
||||||
|
&.seamonkey {
|
||||||
|
background-position: 4px -68px;
|
||||||
|
}
|
||||||
|
&.sunbird {
|
||||||
|
background-position: 4px -103px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a:hover,
|
||||||
|
a.sel,
|
||||||
|
&.sel a.sel:hover {
|
||||||
|
background-color: #EBF4FE;
|
||||||
|
color: #447BC4;
|
||||||
|
}
|
||||||
|
&.sel a:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: @link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.html-rtl #site-search-suggestions .wrap:before {
|
||||||
|
left: auto;
|
||||||
|
right: 14px;
|
||||||
|
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 632 B |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 429 B |
|
@ -0,0 +1,87 @@
|
||||||
|
function objEqual(a, b) {
|
||||||
|
return JSON.stringify(a) == JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
z._AjaxCache = {};
|
||||||
|
z.AjaxCache = (function() {
|
||||||
|
return function(namespace) {
|
||||||
|
if (z._AjaxCache[namespace] === undefined) {
|
||||||
|
z._AjaxCache[namespace] = {
|
||||||
|
'previous': {'args': '', 'data': ''},
|
||||||
|
'items': {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return z._AjaxCache[namespace];
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
|
||||||
|
$.ajaxCache = function(o) {
|
||||||
|
o = $.extend({
|
||||||
|
url: '',
|
||||||
|
type: 'get',
|
||||||
|
data: {}, // Key/value pairs of form data.
|
||||||
|
newItems: $.noop, // Callback upon success of items fetched.
|
||||||
|
cacheSuccess: $.noop, // Callback upon success of items fetched
|
||||||
|
// in cache.
|
||||||
|
ajaxSuccess: $.noop, // Callback upon success of Ajax request.
|
||||||
|
ajaxFailure: $.noop, // Callback upon failure of Ajax request.
|
||||||
|
}, o);
|
||||||
|
|
||||||
|
var cache = z.AjaxCache(o.url + ':' + o.type),
|
||||||
|
args = JSON.stringify(o.data),
|
||||||
|
$self = this,
|
||||||
|
items,
|
||||||
|
request;
|
||||||
|
|
||||||
|
if (args != JSON.stringify(cache.previous.args)) {
|
||||||
|
if (!!cache.items[args]) {
|
||||||
|
items = cache.items[args];
|
||||||
|
if (o.newItems) {
|
||||||
|
o.newItems(null, items);
|
||||||
|
}
|
||||||
|
if (o.cacheSuccess) {
|
||||||
|
o.cacheSuccess(null, items);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Make a request to fetch new items.
|
||||||
|
request = $.ajax({url: o.url, type: o.method, data: o.data});
|
||||||
|
|
||||||
|
request.done(function(data) {
|
||||||
|
var items;
|
||||||
|
if (!objEqual(data, cache.previous.data)) {
|
||||||
|
items = data;
|
||||||
|
}
|
||||||
|
if (o.newItems) {
|
||||||
|
o.newItems(data, items);
|
||||||
|
}
|
||||||
|
if (o.ajaxSuccess) {
|
||||||
|
o.ajaxSuccess(data, items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional failure callback.
|
||||||
|
if (o.failure) {
|
||||||
|
request.fail(function(data) {
|
||||||
|
o.ajaxFailure(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
request.always(function(data) {
|
||||||
|
// Store items returned from this request.
|
||||||
|
cache.items[args] = data;
|
||||||
|
|
||||||
|
// Store current list of items and form data (arguments).
|
||||||
|
cache.previous.data = data;
|
||||||
|
cache.previous.args = args;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
|
@ -0,0 +1,184 @@
|
||||||
|
$(document).ready(function() {
|
||||||
|
if (waffle.switch('search-suggestions')) {
|
||||||
|
$('#search #search-q').searchSuggestions($('#site-search-suggestions'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$.fn.searchSuggestions = function(results) {
|
||||||
|
var $self = this,
|
||||||
|
$form = $self.closest('form'),
|
||||||
|
$results = results;
|
||||||
|
|
||||||
|
// Some base elements that we don't want to keep creating on the fly.
|
||||||
|
$results.html(
|
||||||
|
'<div class="wrap"><p><a class="sel" href="#"></a></p><ul></ul></div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Control keys that shouldn't trigger new requests.
|
||||||
|
var ignoreKeys = [
|
||||||
|
$.ui.keyCode.SHIFT, $.ui.keyCode.CONTROL, $.ui.keyCode.ALT,
|
||||||
|
19, // pause
|
||||||
|
$.ui.keyCode.CAPS_LOCK, $.ui.keyCode.ESCAPE,
|
||||||
|
$.ui.keyCode.PAGE_UP, $.ui.keyCode.PAGE_DOWN,
|
||||||
|
$.ui.keyCode.LEFT, $.ui.keyCode.UP,
|
||||||
|
$.ui.keyCode.RIGHT, $.ui.keyCode.DOWN,
|
||||||
|
$.ui.keyCode.HOME, $.ui.keyCode.END,
|
||||||
|
$.ui.keyCode.COMMAND,
|
||||||
|
92, // right windows key
|
||||||
|
$.ui.keyCode.COMMAND_RIGHT,
|
||||||
|
219, // left windows key (Opera)
|
||||||
|
220, // right windows key (Opera)
|
||||||
|
224 // apple key
|
||||||
|
];
|
||||||
|
|
||||||
|
function pageUp() {
|
||||||
|
// Select the first element.
|
||||||
|
$results.find('.sel').removeClass('sel');
|
||||||
|
$results.removeClass('sel');
|
||||||
|
$results.find('a:first').addClass('sel');
|
||||||
|
}
|
||||||
|
function pageDown() {
|
||||||
|
// Select the last element.
|
||||||
|
$results.find('.sel').removeClass('sel');
|
||||||
|
$results.removeClass('sel');
|
||||||
|
$results.find('a:last').addClass('sel');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissHandler() {
|
||||||
|
$results.removeClass('visible sel');
|
||||||
|
$results.find('.sel').removeClass('sel');
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowHandler(e) {
|
||||||
|
if (e.which == $.ui.keyCode.UP || e.which == $.ui.keyCode.DOWN) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $sel = $results.find('.sel'),
|
||||||
|
$elems = $results.find('a'),
|
||||||
|
i = $elems.index($sel.get(0));
|
||||||
|
|
||||||
|
if ($sel.length && i >= 0) {
|
||||||
|
if (e.which == $.ui.keyCode.UP) {
|
||||||
|
// Clamp the value so it goes to the previous row
|
||||||
|
// but never goes beyond the first row.
|
||||||
|
i = Math.max(0, i - 1);
|
||||||
|
} else {
|
||||||
|
// Clamp the value so it goes to the next row
|
||||||
|
// but never goes beyond the last row.
|
||||||
|
i = Math.min(i + 1, $elems.length - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
$sel.removeClass('sel');
|
||||||
|
$elems.eq(i).addClass('sel');
|
||||||
|
$results.addClass('sel').trigger('selectedRowUpdate', [i]);
|
||||||
|
} else if (e.which == $.ui.keyCode.PAGE_UP ||
|
||||||
|
e.which == $.ui.keyCode.HOME) {
|
||||||
|
e.preventDefault();
|
||||||
|
pageUp();
|
||||||
|
$results.addClass('sel').trigger('selectedRowUpdate', [0]);
|
||||||
|
} else if (e.which == $.ui.keyCode.PAGE_DOWN ||
|
||||||
|
e.which == $.ui.keyCode.END) {
|
||||||
|
e.preventDefault();
|
||||||
|
pageDown();
|
||||||
|
$results.addClass('sel').trigger('selectedRowUpdate',
|
||||||
|
[$results.find('a').length - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputHandler(e) {
|
||||||
|
var val = $self.val();
|
||||||
|
if (val.length < 3) {
|
||||||
|
$results.filter('.visible').removeClass('visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($.inArray(e.which, ignoreKeys) !== -1) {
|
||||||
|
$results.trigger('inputIgnored');
|
||||||
|
} else {
|
||||||
|
var msg = format(gettext('Search add-ons for <b>"{0}"</b>'), val);
|
||||||
|
$results.find('p a').html(msg);
|
||||||
|
|
||||||
|
var li_item = template(
|
||||||
|
'<li><a href="{url}" {icon} {cls}>{name}</a></li>'
|
||||||
|
);
|
||||||
|
|
||||||
|
$.ajaxCache({
|
||||||
|
url: $results.attr('data-src'),
|
||||||
|
data: $form.serialize(),
|
||||||
|
newItems: function(formdata, items) {
|
||||||
|
var eventName;
|
||||||
|
if (items !== undefined) {
|
||||||
|
var ul = '';
|
||||||
|
$.each(items, function(i, item) {
|
||||||
|
var d = {url: item.url || '#', icon: '', cls: ''};
|
||||||
|
if (item.icon) {
|
||||||
|
d.icon = format(
|
||||||
|
'style="background-image:url({0})"',
|
||||||
|
item.icon);
|
||||||
|
}
|
||||||
|
if (item.cls) {
|
||||||
|
d.cls = format('class="{0}"', item.cls);
|
||||||
|
}
|
||||||
|
if (item.name) {
|
||||||
|
d.name = item.name;
|
||||||
|
ul += li_item(d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$results.find('ul').html(ul);
|
||||||
|
}
|
||||||
|
highlight(val);
|
||||||
|
$results.trigger('resultsUpdated', [items]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.which == $.ui.keyCode.ESCAPE) {
|
||||||
|
dismissHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$self.blur(dismissHandler)
|
||||||
|
.keydown(rowHandler)
|
||||||
|
.bind('keyup input paste', _.throttle(inputHandler, 250));
|
||||||
|
|
||||||
|
$results.delegate('li', 'hover', function() {
|
||||||
|
$results.find('.sel').removeClass('sel');
|
||||||
|
$results.addClass('sel');
|
||||||
|
$(this).find('a').addClass('sel');
|
||||||
|
}).delegate('p a', 'click', _pd(function() {
|
||||||
|
$form.submit();
|
||||||
|
}));
|
||||||
|
|
||||||
|
$form.submit(function(e) {
|
||||||
|
var $sel = $results.find('.sel');
|
||||||
|
if ($sel.length && $sel.eq(0).attr('href') != '#') {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
window.location = $sel.get(0).href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).keyup(function(e) {
|
||||||
|
if (e.which == 16 || e.which == 83) {
|
||||||
|
$self.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function highlight(val) {
|
||||||
|
// If an item starts with `val`, wrap the matched text with boldness.
|
||||||
|
var pat = new RegExp('\\b' + val, 'gi');
|
||||||
|
$results.find('ul a').each(function() {
|
||||||
|
var $this = $(this),
|
||||||
|
txt = $this.text(),
|
||||||
|
matchedTxt = txt.replace(pat, '<b>$&</b>');
|
||||||
|
if (txt != matchedTxt) {
|
||||||
|
$this.html(matchedTxt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$results.addClass('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
|
@ -149,3 +149,19 @@ tests.lacksClass = function($sel, cls) {
|
||||||
equals($sel.hasClass(cls), false,
|
equals($sel.hasClass(cls), false,
|
||||||
'Should not have ' + cls + ', got: ' + $sel.attr('class'));
|
'Should not have ' + cls + ', got: ' + $sel.attr('class'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tests.equalObjects = function(a, b) {
|
||||||
|
/*
|
||||||
|
Asserts that two objects are equal by comparing serialized strings.
|
||||||
|
deepEqual is stupid and flaky.
|
||||||
|
*/
|
||||||
|
equal(JSON.stringify(a), JSON.stringify(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.notEqualObjects = function(a, b) {
|
||||||
|
/*
|
||||||
|
Asserts that two objects are unequal by comparing serialized strings.
|
||||||
|
notDeepEqual is stupid and flaky.
|
||||||
|
*/
|
||||||
|
notEqual(JSON.stringify(a), JSON.stringify(b));
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
module('Ajax Cache', {
|
||||||
|
setup: function() {
|
||||||
|
this.newItems = {'ajax': [], 'cache': []};
|
||||||
|
z._AjaxCache = {};
|
||||||
|
$.mockjaxClear();
|
||||||
|
$.mockjaxSettings = {
|
||||||
|
status: 200,
|
||||||
|
responseTime: 0,
|
||||||
|
contentType: 'text/json',
|
||||||
|
dataType: 'json'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
teardown: function() {
|
||||||
|
$.mockjaxClear();
|
||||||
|
},
|
||||||
|
query: function(term, url) {
|
||||||
|
var self = this,
|
||||||
|
results = [];
|
||||||
|
if (url) {
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
results.push({'id': i, 'url': url});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url = '/cacheMoney';
|
||||||
|
results = [
|
||||||
|
{'id': 1, 'url': 'gkoberger.net'},
|
||||||
|
{'id': 2, 'url': 'gkoberger.net'},
|
||||||
|
{'id': 3, 'url': 'gkoberger.net'}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$.mockjax({
|
||||||
|
url: url,
|
||||||
|
responseText: JSON.stringify(results),
|
||||||
|
status: 200,
|
||||||
|
responseTime: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.ajaxCalled = false;
|
||||||
|
this.cacheCalled = false;
|
||||||
|
return {
|
||||||
|
url: url,
|
||||||
|
data: {'q': term},
|
||||||
|
ajaxSuccess: function(data, items) {
|
||||||
|
self.newItems['ajax'].push(items);
|
||||||
|
self.ajaxCalled = true;
|
||||||
|
},
|
||||||
|
cacheSuccess: function(data, items) {
|
||||||
|
self.newItems['cache'].push(items);
|
||||||
|
self.cacheCalled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
is_ajax: function() {
|
||||||
|
equal(this.ajaxCalled, true);
|
||||||
|
equal(this.cacheCalled, false);
|
||||||
|
},
|
||||||
|
is_cache: function() {
|
||||||
|
equal(this.ajaxCalled, false);
|
||||||
|
equal(this.cacheCalled, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('New request', function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
$.ajaxCache(self.query('some term')).done(function() {
|
||||||
|
self.is_ajax();
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Identical requests', function() {
|
||||||
|
var self = this,
|
||||||
|
request1,
|
||||||
|
request2;
|
||||||
|
|
||||||
|
request1 = $.ajaxCache(self.query('some term')).done(function() {
|
||||||
|
self.is_ajax();
|
||||||
|
|
||||||
|
// This request should be cached.
|
||||||
|
request2 = $.ajaxCache(self.query('some term'));
|
||||||
|
self.is_cache();
|
||||||
|
|
||||||
|
// Ensure that we returned the correct items.
|
||||||
|
tests.equalObjects(self.newItems['ajax'], self.newItems['cache']);
|
||||||
|
|
||||||
|
// When the request is cached, we don't return an $.ajax request.
|
||||||
|
equal(request2, undefined);
|
||||||
|
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Same URLs, unique parameters, same results', function() {
|
||||||
|
var self = this,
|
||||||
|
request1,
|
||||||
|
request2,
|
||||||
|
request3;
|
||||||
|
|
||||||
|
request1 = $.ajaxCache(self.query('some term')).done(function() {
|
||||||
|
// This is a cached request.
|
||||||
|
request2 = $.ajaxCache(self.query('some term'));
|
||||||
|
|
||||||
|
// This is a request with new parameters but will return same items.
|
||||||
|
request3 = $.ajaxCache(self.query('new term')).done(function() {
|
||||||
|
self.is_ajax();
|
||||||
|
|
||||||
|
// We return `undefined` when items remain unchanged.
|
||||||
|
equal(self.newItems['ajax'][1], undefined);
|
||||||
|
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Unique URLs, same parameters, unique results', function() {
|
||||||
|
var self = this,
|
||||||
|
request1,
|
||||||
|
request2;
|
||||||
|
|
||||||
|
request1 = $.ajaxCache(self.query('some term', 'poop')).done(function() {
|
||||||
|
self.is_ajax();
|
||||||
|
|
||||||
|
// This is a new request with a different URL.
|
||||||
|
request2 = $.ajaxCache(self.query('some term', 'crap')).done(function() {
|
||||||
|
self.is_ajax();
|
||||||
|
tests.notEqualObjects(self.newItems['ajax'][0],
|
||||||
|
self.newItems['ajax'][1]);
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Unique URLs, unique parameters, unique results', function() {
|
||||||
|
var self = this,
|
||||||
|
request1,
|
||||||
|
request2;
|
||||||
|
|
||||||
|
request1 = $.ajaxCache(self.query('some term', 'poop')).done(function() {
|
||||||
|
self.is_ajax();
|
||||||
|
|
||||||
|
// This is a new request with a different URL and different parameters.
|
||||||
|
request2 = $.ajaxCache(self.query('diff term', 'crap')).done(function() {
|
||||||
|
self.is_ajax();
|
||||||
|
tests.notEqualObjects(self.newItems['ajax'][0],
|
||||||
|
self.newItems['ajax'][1]);
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,133 @@
|
||||||
|
module('Search Suggestions', {
|
||||||
|
setup: function() {
|
||||||
|
this.sandbox = tests.createSandbox('#search-suggestions');
|
||||||
|
this.results = $('#site-search-suggestions', this.sandbox);
|
||||||
|
this.input = $('#search #search-q', this.sandbox);
|
||||||
|
this.input.searchSuggestions(this.results);
|
||||||
|
this.url = this.results.attr('data-src');
|
||||||
|
|
||||||
|
this.newItems = {'ajax': [], 'cache': []};
|
||||||
|
z._AjaxCache = {};
|
||||||
|
$.mockjaxClear();
|
||||||
|
$.mockjaxSettings = {
|
||||||
|
status: 200,
|
||||||
|
responseTime: 0,
|
||||||
|
contentType: 'text/json',
|
||||||
|
dataType: 'json'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
teardown: function() {
|
||||||
|
this.sandbox.remove();
|
||||||
|
$.mockjaxClear();
|
||||||
|
},
|
||||||
|
mockRequest: function() {
|
||||||
|
this.jsonResults = [];
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
this.jsonResults.push({'id': i, 'url': 'dekKobergerStudios.biz'});
|
||||||
|
}
|
||||||
|
$.mockjax({
|
||||||
|
url: this.url,
|
||||||
|
responseText: JSON.stringify(this.jsonResults),
|
||||||
|
status: 200,
|
||||||
|
responseTime: 0
|
||||||
|
});
|
||||||
|
},
|
||||||
|
testInputEvent: function(eventType, fail) {
|
||||||
|
var self = this,
|
||||||
|
$input = self.input,
|
||||||
|
$results = self.results,
|
||||||
|
query = 'xxx';
|
||||||
|
self.mockRequest();
|
||||||
|
if (fail) {
|
||||||
|
var inputIgnored = false;
|
||||||
|
// If we send press a bad key, this will check that we ignored it.
|
||||||
|
self.sandbox.bind('inputIgnored', function(e) {
|
||||||
|
inputIgnored = true;
|
||||||
|
});
|
||||||
|
tests.waitFor(function() {
|
||||||
|
return inputIgnored;
|
||||||
|
}).thenDo(function() {
|
||||||
|
ok(inputIgnored);
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.sandbox.bind('resultsUpdated', function(e, items) {
|
||||||
|
tests.equalObjects(items, self.jsonResults);
|
||||||
|
equal($results.find('.wrap p a.sel b').text(),
|
||||||
|
'"' + query + '"');
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$input.val(query);
|
||||||
|
$input.triggerHandler(eventType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('Generated HTML tags', function() {
|
||||||
|
var $results = this.results,
|
||||||
|
$sel = $results.find('.wrap p a.sel');
|
||||||
|
equal($sel.length, 1);
|
||||||
|
equal($sel.find('b').length, 0);
|
||||||
|
equal($results.find('.wrap ul').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Results upon good keyup', function() {
|
||||||
|
this.testInputEvent({type: 'keyup', which: 'x'.charCodeAt(0)});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Results upon bad keyup', function() {
|
||||||
|
this.testInputEvent({type: 'keyup', which: 16}, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Results upon input', function() {
|
||||||
|
this.testInputEvent('input');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Results upon paste', function() {
|
||||||
|
this.testInputEvent('paste');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Hide results upon escape/blur', function() {
|
||||||
|
var self = this,
|
||||||
|
$input = self.input,
|
||||||
|
$results = self.results;
|
||||||
|
$input.val('xxx');
|
||||||
|
$input.triggerHandler('blur');
|
||||||
|
tests.lacksClass($results, 'visible');
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
asyncTest('Cached results do not change', function() {
|
||||||
|
var self = this,
|
||||||
|
$input = self.input,
|
||||||
|
$results = self.results,
|
||||||
|
query = 'xxx';
|
||||||
|
self.mockRequest();
|
||||||
|
self.sandbox.bind('resultsUpdated', function(e, items) {
|
||||||
|
equal($results.find('.wrap p a.sel b').text(), '"' + query + '"');
|
||||||
|
tests.equalObjects(items, self.jsonResults);
|
||||||
|
if (z._AjaxCache === undefined) {
|
||||||
|
$input.triggerHandler('paste');
|
||||||
|
} else {
|
||||||
|
tests.waitFor(function() {
|
||||||
|
return z._AjaxCache;
|
||||||
|
}).thenDo(function() {
|
||||||
|
var cache = z.AjaxCache(self.url + ':get'),
|
||||||
|
args = JSON.stringify(self.sandbox.find('form').serialize());
|
||||||
|
tests.equalObjects(cache.items[args], items);
|
||||||
|
tests.equalObjects(cache.previous.data, items);
|
||||||
|
equal(cache.previous.args, args);
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$input.val(query);
|
||||||
|
$input.triggerHandler('paste');
|
||||||
|
});
|
|
@ -16,6 +16,8 @@
|
||||||
"js/zamboni/contributions.js",
|
"js/zamboni/contributions.js",
|
||||||
"js/zamboni/password-strength.js",
|
"js/zamboni/password-strength.js",
|
||||||
"js/impala/persona_creation.js",
|
"js/impala/persona_creation.js",
|
||||||
"js/zamboni/browserid_support.js"
|
"js/zamboni/browserid_support.js",
|
||||||
|
"js/impala/ajaxcache.js",
|
||||||
|
"js/impala/search.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
INSERT INTO waffle_switch (name, active) VALUES ('search-suggestions', 0);
|
|
@ -470,6 +470,7 @@ MINIFY_BUNDLES = {
|
||||||
'css/impala/collections.less',
|
'css/impala/collections.less',
|
||||||
'css/impala/tooltips.less',
|
'css/impala/tooltips.less',
|
||||||
'css/impala/search.less',
|
'css/impala/search.less',
|
||||||
|
'css/impala/suggestions.less',
|
||||||
'css/impala/colorpicker.less',
|
'css/impala/colorpicker.less',
|
||||||
'css/impala/personas.less',
|
'css/impala/personas.less',
|
||||||
'css/impala/login.less',
|
'css/impala/login.less',
|
||||||
|
@ -600,6 +601,7 @@ MINIFY_BUNDLES = {
|
||||||
'js/lib/jquery-ui/jquery.ui.sortable.js',
|
'js/lib/jquery-ui/jquery.ui.sortable.js',
|
||||||
|
|
||||||
'js/zamboni/truncation.js',
|
'js/zamboni/truncation.js',
|
||||||
|
'js/impala/ajaxcache.js',
|
||||||
'js/zamboni/global.js',
|
'js/zamboni/global.js',
|
||||||
'js/impala/global.js',
|
'js/impala/global.js',
|
||||||
'js/impala/ratingwidget.js',
|
'js/impala/ratingwidget.js',
|
||||||
|
@ -644,6 +646,7 @@ MINIFY_BUNDLES = {
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
'js/impala/search.js',
|
'js/impala/search.js',
|
||||||
|
'js/impala/suggestions.js',
|
||||||
|
|
||||||
# Fix-up outgoing links
|
# Fix-up outgoing links
|
||||||
'js/zamboni/outgoing_links.js',
|
'js/zamboni/outgoing_links.js',
|
||||||
|
|
|
@ -196,6 +196,7 @@
|
||||||
{# js #}
|
{# js #}
|
||||||
{% block site_js %}
|
{% block site_js %}
|
||||||
<script src="{{ static(url('jsi18n')) }}"></script>
|
<script src="{{ static(url('jsi18n')) }}"></script>
|
||||||
|
<script src="{{ url('wafflejs') }}"></script>
|
||||||
{{ js('impala') }}
|
{{ js('impala') }}
|
||||||
<script async defer src="{{ static(url('addons.buttons.js')) }}"></script>
|
<script async defer src="{{ static(url('addons.buttons.js')) }}"></script>
|
||||||
<script async defer src="{{ settings.PAYPAL_JS_URL }}"></script>
|
<script async defer src="{{ settings.PAYPAL_JS_URL }}"></script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% set search_form = SimpleSearchForm(request, search_cat) %}
|
{% set search_form = SimpleSearchForm(request, search_cat) %}
|
||||||
<form id="search" action="{{ search_url|default(url('search.search')) }}">
|
<form id="search" action="{{ search_url|default(url('search.search')) }}">
|
||||||
<input id="search-q" type="text" name="q" required title=""
|
<input id="search-q" type="text" name="q" required autocomplete="off" title=""
|
||||||
class="text {% if not search_form.q.data %}placeholder{% endif %}"
|
class="text {% if not search_form.q.data %}placeholder{% endif %}"
|
||||||
placeholder="{{ search_form.placeholder() }}"
|
placeholder="{{ search_form.placeholder() }}"
|
||||||
value="{{ search_form.q.data or '' }}">
|
value="{{ search_form.q.data or '' }}">
|
||||||
|
@ -9,4 +9,8 @@
|
||||||
{{ search_form.platform }}
|
{{ search_form.platform }}
|
||||||
<button class="search-button" type="submit" title="{{ _('Search') }}"
|
<button class="search-button" type="submit" title="{{ _('Search') }}"
|
||||||
src="{{ media('img/zamboni/global/btn-search.png') }}"></button>
|
src="{{ media('img/zamboni/global/btn-search.png') }}"></button>
|
||||||
|
{% if waffle.switch('search-suggestions') %}
|
||||||
|
<div id="site-search-suggestions"
|
||||||
|
data-src="{{ url('search.suggestions') }}"></div>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -155,4 +155,13 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="search-suggestions">
|
||||||
|
<form id="search" action="/en-us/firefox/search/">
|
||||||
|
<input id="search-q" type="text" name="q" required autocomplete="off" title=""
|
||||||
|
class="text placeholder" placeholder="search for add-ons" value="">
|
||||||
|
<input type="hidden" name="cat" value="all" id="id_cat">
|
||||||
|
<div id="site-search-suggestions" data-src="/askjeeves"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче