Skip to content

Commit

Permalink
feature #632 Fix jQuery instantSearch plugin and search result (yceruto)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the master branch (closes #632).

Discussion
----------

Fix jQuery instantSearch plugin and search result

![search](https://user-images.githubusercontent.com/2028198/29907671-c766eaa8-8dea-11e7-97fb-78ff51a7a33e.png)

**instantSearch**
 * Refactored to some known JavaScript pattern (implemented Prototype Design Pattern)
 * Allow override options through input attributes `data-*` and added support to text translation ("No results found")
 * Add article template prototype and render method (improved visibility)
 * Sent configured limit of items to search controller (this allows to configure more than 10 items as results from plugin)
 * Simplify properties and methods (improved visibility)

**Search result**
 * Removed old effect of hide/show results on focus out/in of the input
 * Show publishedDate and author for each article found
 * Sync style with blog index page and some margin adjustments

**Minor unrelated changes**
 * Fix @return type-hints for `PostRepository::findBySearchQuery()` method (better autocomplete)
 * Show logout link as last link always

Commits
-------

ea96338 Fix jQuery instantSearch plugin and search result
  • Loading branch information
javiereguiluz committed Aug 31, 2017
2 parents 1123e9b + ea96338 commit 2fbc7f7
Show file tree
Hide file tree
Showing 17 changed files with 147 additions and 586 deletions.
137 changes: 75 additions & 62 deletions assets/js/jquery.instantSearch.js
Expand Up @@ -2,92 +2,105 @@
* jQuery plugin for an instant searching.
*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
(function($) {
$.fn.instantSearch = function(config) {
return this.each(function() {
initInstantSearch(this, $.extend(true, defaultConfig, config || {}));
});
(function ($) {
'use strict';

String.prototype.render = function (parameters) {
return this.replace(/({{ (\w+) }})/g, function (match, pattern, name) {
return parameters[name];
})
};

var defaultConfig = {
// INSTANTS SEARCH PUBLIC CLASS DEFINITION
// =======================================

var InstantSearch = function (element, options) {
this.$input = $(element);
this.$form = this.$input.closest('form');
this.$preview = $('<ul class="search-preview list-group">').appendTo(this.$form);
this.options = $.extend({}, InstantSearch.DEFAULTS, this.$input.data(), options);

this.$input.keyup(this.debounce());
};

InstantSearch.DEFAULTS = {
minQueryLength: 2,
maxPreviewItems: 10,
previewDelay: 500,
noItemsFoundMessage: 'No results found.'
limit: 10,
delay: 500,
noResultsMessage: 'No results found',
itemTemplate: '\
<article class="post">\
<h2><a href="{{ url }}">{{ title }}</a></h2>\
<p class="post-metadata">\
<span class="metadata"><i class="fa fa-calendar"></i> {{ date }}</span>\
<span class="metadata"><i class="fa fa-user"></i> {{ author }}</span>\
</p>\
<p>{{ summary }}</p>\
</article>'
};

function debounce(fn, delay) {
InstantSearch.prototype.debounce = function () {
var delay = this.options.delay;
var search = this.search;
var timer = null;
var self = this;

return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
search.apply(self);
}, delay);
};
}

var initInstantSearch = function(el, config) {
var $input = $(el);
var $form = $input.closest('form');
var $preview = $('<ul class="search-preview list-group">').appendTo($form);

var setPreviewItems = function(items) {
$preview.empty();

$.each(items, function(index, item) {
if (index > config.maxPreviewItems) {
return;
}
};

addItemToPreview(item);
});
InstantSearch.prototype.search = function () {
var query = $.trim(this.$input.val()).replace(/\s{2,}/g, ' ');
if (query.length < this.options.minQueryLength) {
this.$preview.empty();
return;
}

var addItemToPreview = function(item) {
var $link = $('<a>').attr('href', item.url).text(item.title);
var $title = $('<h3>').attr('class', 'm-b-0').append($link);
var $summary = $('<p>').text(item.summary);
var $result = $('<div>').append($title).append($summary);
var self = this;
var data = this.$form.serializeArray();
data['l'] = this.limit;

$preview.append($result);
}
$.getJSON(this.$form.attr('action'), data, function (items) {
self.show(items);
});
};

var noItemsFound = function() {
var $result = $('<div>').text(config.noItemsFoundMessage);
InstantSearch.prototype.show = function (items) {
var $preview = this.$preview;
var itemTemplate = this.options.itemTemplate;

if (0 === items.length) {
$preview.html(this.options.noResultsMessage);
} else {
$preview.empty();
$preview.append($result);
$.each(items, function (index, item) {
$preview.append(itemTemplate.render(item));
});
}
};

var updatePreview = function() {
var query = $.trim($input.val()).replace(/\s{2,}/g, ' ');
// INSTANTS SEARCH PLUGIN DEFINITION
// =================================

if (query.length < config.minQueryLength) {
$preview.empty();
return;
}
function Plugin(option) {
return this.each(function () {
var $this = $(this);
var instance = $this.data('instantSearch');
var options = typeof option === 'object' && option;

$.getJSON($form.attr('action') + '?' + $form.serialize(), function(items) {
if (items.length === 0) {
noItemsFound();
return;
}
if (!instance) $this.data('instantSearch', (instance = new InstantSearch(this, options)));

setPreviewItems(items);
});
}

$input.focusout(function(e) {
$preview.fadeOut();
});
if (option === 'search') instance.search();
})
}

$input.focusin(function(e) {
$preview.fadeIn();
updatePreview();
});
$.fn.instantSearch = Plugin;
$.fn.instantSearch.Constructor = InstantSearch;

$input.keyup(debounce(updatePreview, config.previewDelay));
}
})(window.jQuery);
2 changes: 1 addition & 1 deletion assets/js/search.js
Expand Up @@ -2,6 +2,6 @@ import './jquery.instantSearch.js';

$(function() {
$('.search-field').instantSearch({
previewDelay: 100,
delay: 100,
});
});
20 changes: 20 additions & 0 deletions assets/scss/app.scss
Expand Up @@ -308,3 +308,23 @@ body#comment_form_error h1.text-danger {
width: 98%;
}
}

/* Page: 'Blog search'
------------------------------------------------------------------------- */
body#blog_search #main h1,
body#blog_search #main p {
margin-bottom: 0.5em
}

body#blog_search article.post:first-child {
margin-top: 2em;
}

body#blog_search article.post {
margin-bottom: 2em;
}

body#blog_search .post-metadata {
font-size: 16px;
margin-bottom: 8px;
}
2 changes: 1 addition & 1 deletion public/build/css/app.css

Large diffs are not rendered by default.

482 changes: 13 additions & 469 deletions public/build/js/admin.js

Large diffs are not rendered by default.

26 changes: 1 addition & 25 deletions public/build/js/app.js

Large diffs are not rendered by default.

10 changes: 2 additions & 8 deletions public/build/js/common.js

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions public/build/js/login.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 1 addition & 7 deletions public/build/js/search.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion public/build/manifest.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion public/build/manifest.json
Expand Up @@ -25,4 +25,4 @@
"build/js/login.js": "/build/js/login.js",
"build/js/search.js": "/build/js/search.js",
"build/manifest.js": "/build/manifest.js"
}
}
5 changes: 4 additions & 1 deletion src/Controller/BlogController.php
Expand Up @@ -165,12 +165,15 @@ public function searchAction(Request $request)
}

$query = $request->query->get('q', '');
$posts = $this->getDoctrine()->getRepository(Post::class)->findBySearchQuery($query);
$limit = $request->query->get('l', 10);
$posts = $this->getDoctrine()->getRepository(Post::class)->findBySearchQuery($query, $limit);

$results = [];
foreach ($posts as $post) {
$results[] = [
'title' => htmlspecialchars($post->getTitle()),
'date' => $post->getPublishedAt()->format('M d, Y'),
'author' => htmlspecialchars($post->getAuthor()->getFullName()),
'summary' => htmlspecialchars($post->getSummary()),
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
];
Expand Down
2 changes: 1 addition & 1 deletion src/Repository/PostRepository.php
Expand Up @@ -64,7 +64,7 @@ private function createPaginator(Query $query, $page)
* @param string $rawQuery The search query as input by the user
* @param int $limit The maximum number of results returned
*
* @return array
* @return Post[]
*/
public function findBySearchQuery($rawQuery, $limit = Post::NUM_ITEMS)
{
Expand Down
8 changes: 4 additions & 4 deletions templates/base.html.twig
Expand Up @@ -56,6 +56,10 @@
{% endif %}
{% endblock %}

<li>
<a href="{{ path('blog_search') }}"> <i class="fa fa-search"></i> {{ 'menu.search'|trans }}</a>
</li>

{% if app.user %}
<li>
<a href="{{ path('security_logout') }}">
Expand All @@ -64,10 +68,6 @@
</li>
{% endif %}

<li>
<a href="{{ path('blog_search') }}"> <i class="fa fa-search"></i> {{ 'menu.search'|trans }}</a>
</li>

<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" id="locales">
<i class="fa fa-globe" aria-hidden="true"></i>
Expand Down
10 changes: 9 additions & 1 deletion templates/blog/search.html.twig
Expand Up @@ -5,10 +5,18 @@
<script src="{{ asset('build/js/search.js') }}"></script>
{% endblock %}

{% block body_id 'blog_search' %}

{% block main %}
<form action="{{ path('blog_search') }}" method="get">
<div class="form-group">
<input name="q" type="text" class="form-control search-field" placeholder="{{ 'post.search_for'|trans }}" autocomplete="off" autofocus>
<input name="q"
class="form-control search-field"
placeholder="{{ 'post.search_for'|trans }}"
autocomplete="off"
autofocus
data-no-results-message="{{ 'post.search_no_results'|trans }}"
>
</div>
</form>

Expand Down
4 changes: 4 additions & 0 deletions translations/messages.en.xlf
Expand Up @@ -296,6 +296,10 @@
<source>post.search_for</source>
<target>Search for...</target>
</trans-unit>
<trans-unit id="post.search_no_results">
<source>post.search_no_results</source>
<target>No results found</target>
</trans-unit>

<trans-unit id="notification.comment_created">
<source>notification.comment_created</source>
Expand Down
8 changes: 8 additions & 0 deletions translations/messages.es.xlf
Expand Up @@ -329,6 +329,14 @@
<source>post.deleted_successfully</source>
<target>¡Artículo eliminado con éxito!</target>
</trans-unit>
<trans-unit id="post.search_for">
<source>post.search_for</source>
<target>Buscar...</target>
</trans-unit>
<trans-unit id="post.search_no_results">
<source>post.search_no_results</source>
<target>No se encontraron resultados</target>
</trans-unit>
<trans-unit id="notification.comment_created">
<source>notification.comment_created</source>
<target>¡Su artículo recibió un comentario!</target>
Expand Down

0 comments on commit 2fbc7f7

Please sign in to comment.