Skip to content

Commit 3194067

Browse files
committed
Add an instant post search
1 parent aca1d44 commit 3194067

File tree

7 files changed

+195
-1
lines changed

7 files changed

+195
-1
lines changed

app/Resources/translations/messages.en.xlf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@
284284
<source>post.deleted_successfully</source>
285285
<target>Post deleted successfully!</target>
286286
</trans-unit>
287+
<trans-unit id="post.search_for">
288+
<source>post.search_for</source>
289+
<target>Search for...</target>
290+
</trans-unit>
287291

288292
<trans-unit id="notification.comment_created">
289293
<source>notification.comment_created</source>

app/Resources/translations/messages.ru.xlf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@
284284
<source>post.deleted_successfully</source>
285285
<target>Запись успешно удалена!</target>
286286
</trans-unit>
287+
<trans-unit id="post.search_for">
288+
<source>post.search_for</source>
289+
<target>Искать запись...</target>
290+
</trans-unit>
287291

288292
<trans-unit id="notification.comment_created">
289293
<source>notification.comment_created</source>

app/Resources/views/base.html.twig

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<header>
2929
<div class="navbar navbar-default navbar-static-top" role="navigation">
3030
<div class="container">
31-
<div class="navbar-header">
31+
<div class="navbar-header col-md-3 col-lg-2">
3232
<a class="navbar-brand" href="{{ path('homepage') }}">
3333
Symfony Demo
3434
</a>
@@ -42,6 +42,13 @@
4242
<span class="icon-bar"></span>
4343
</button>
4444
</div>
45+
<div class="search-bar col-sm-5">
46+
<form action="{{ path('blog_search') }}" method="get">
47+
<div class="input-group-sm">
48+
<input name="q" type="text" class="form-control" placeholder="{{ 'post.search_for'|trans }}" autocomplete="off">
49+
</div>
50+
</form>
51+
</div>
4552
<div class="navbar-collapse collapse">
4653
<ul class="nav navbar-nav navbar-right">
4754

@@ -143,7 +150,15 @@
143150
<script src="{{ asset('js/highlight.pack.js') }}"></script>
144151
<script src="{{ asset('js/bootstrap-datetimepicker.min.js') }}"></script>
145152
<script src="{{ asset('js/bootstrap-tagsinput.min.js') }}"></script>
153+
<script src="{{ asset('js/jquery.instantSearch.js') }}"></script>
146154
<script src="{{ asset('js/main.js') }}"></script>
155+
<script>
156+
$(function() {
157+
$('.search-bar input[name="q"]').instantSearch({
158+
noItemsFoundMessage: '{{ 'post.no_posts_found'|trans }}'
159+
});
160+
});
161+
</script>
147162
{% endblock %}
148163

149164
{# it's not mandatory to set the timezone in localizeddate(). This is done to

src/AppBundle/Controller/BlogController.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
2323
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
2424
use Symfony\Component\EventDispatcher\GenericEvent;
25+
use Symfony\Component\HttpFoundation\JsonResponse;
2526
use Symfony\Component\HttpFoundation\Request;
2627
use Symfony\Component\HttpFoundation\Response;
2728

@@ -149,4 +150,42 @@ public function commentFormAction(Post $post)
149150
'form' => $form->createView(),
150151
]);
151152
}
153+
154+
/**
155+
* @Route("/search", name="blog_search")
156+
* @Method("GET")
157+
*
158+
* @return JsonResponse
159+
*/
160+
public function searchAction(Request $request)
161+
{
162+
$query = $request->query->get('q', '');
163+
164+
// Sanitizing the query: removes all non-alphanumeric characters except whitespaces
165+
$query = preg_replace('/[^[:alnum:] ]/', '', trim(preg_replace('/[[:space:]]+/', ' ', $query)));
166+
167+
// Splits the query into terms and removes all terms which
168+
// length is less than 2
169+
$terms = array_unique(explode(' ', strtolower($query)));
170+
$terms = array_filter($terms, function ($term) {
171+
return 2 <= strlen($term);
172+
});
173+
174+
$posts = [];
175+
176+
if (!empty($terms)) {
177+
$posts = $this->getDoctrine()->getRepository(Post::class)->findByTerms($terms);
178+
}
179+
180+
$results = [];
181+
182+
foreach ($posts as $post) {
183+
array_push($results, [
184+
'result' => htmlspecialchars($post->getTitle()),
185+
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
186+
]);
187+
}
188+
189+
return new JsonResponse($results);
190+
}
152191
}

src/AppBundle/Repository/PostRepository.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,22 @@ private function createPaginator(Query $query, $page)
5858

5959
return $paginator;
6060
}
61+
62+
public function findByTerms(array $terms, $limit = Post::NUM_ITEMS)
63+
{
64+
$queryBuilder = $this->createQueryBuilder('p');
65+
66+
foreach ($terms as $key => $term) {
67+
$queryBuilder
68+
->orWhere('p.title LIKE :t_'.$key)
69+
->setParameter('t_'.$key, '%'.$term.'%')
70+
;
71+
}
72+
73+
return $queryBuilder
74+
->orderBy('p.publishedAt', 'DESC')
75+
->setMaxResults($limit)
76+
->getQuery()
77+
->getResult();
78+
}
6179
}

web/css/main.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ header .locales a {
5858
padding-right: 10px
5959
}
6060

61+
header .search-bar {
62+
padding: 0.8em 0;
63+
}
64+
65+
header .search-bar form {
66+
position: relative;
67+
}
68+
69+
header .search-preview {
70+
position: absolute;
71+
width: 100%;
72+
top: 100%;
73+
}
74+
6175
.body-container {
6276
-webkit-flex: 1;
6377
flex: 1;

web/js/jquery.instantSearch.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
/**
11+
* jQuery plugin for an instant searching.
12+
*
13+
* @author Oleg Voronkovich <[email protected]>
14+
*/
15+
(function($) {
16+
$.fn.instantSearch = function(config) {
17+
return this.each(function() {
18+
initInstantSearch(this, $.extend(true, defaultConfig, config || {}));
19+
});
20+
};
21+
22+
var defaultConfig = {
23+
minQueryLength: 2,
24+
maxPreviewItems: 10,
25+
previewDelay: 500,
26+
noItemsFoundMessage: 'No items found'
27+
};
28+
29+
function debounce(fn, delay) {
30+
var timer = null;
31+
return function () {
32+
var context = this, args = arguments;
33+
clearTimeout(timer);
34+
timer = setTimeout(function () {
35+
fn.apply(context, args);
36+
}, delay);
37+
};
38+
}
39+
40+
var initInstantSearch = function(el, config) {
41+
var $input = $(el);
42+
var $form = $input.closest('form');
43+
var $preview = $('<ul class="search-preview list-group">').appendTo($form);
44+
45+
var setPreviewItems = function(items) {
46+
$preview.empty();
47+
48+
$.each(items, function(index, item) {
49+
if (index > config.maxPreviewItems) {
50+
return;
51+
}
52+
53+
addItemToPreview(item);
54+
});
55+
}
56+
57+
var addItemToPreview = function(item) {
58+
var $link = $('<a>').attr('href', item.url).text(item.result);
59+
var $li = $('<li class="list-group-item">').append($link);
60+
61+
$preview.append($li);
62+
}
63+
64+
var noItemsFound = function() {
65+
var $li = $('<li class="list-group-item">').text(config.noItemsFoundMessage);
66+
67+
$preview.empty();
68+
$preview.append($li);
69+
}
70+
71+
var updatePreview = function() {
72+
var query = $.trim($input.val()).replace(/\s{2,}/g, ' ');
73+
74+
if (query.length < config.minQueryLength) {
75+
$preview.empty();
76+
return;
77+
}
78+
79+
$.getJSON($form.attr('action') + '?' + $form.serialize(), function(items) {
80+
if (items.length === 0) {
81+
noItemsFound();
82+
return;
83+
}
84+
85+
setPreviewItems(items);
86+
});
87+
}
88+
89+
$input.focusout(function(e) {
90+
$preview.fadeOut();
91+
});
92+
93+
$input.focusin(function(e) {
94+
$preview.fadeIn();
95+
updatePreview();
96+
});
97+
98+
$input.keyup(debounce(updatePreview, config.previewDelay));
99+
}
100+
})(window.jQuery);

0 commit comments

Comments
 (0)