Skip to content

Commit f45c31c

Browse files
committed
Add an instant post search
1 parent 899859a commit f45c31c

File tree

10 files changed

+215
-8
lines changed

10 files changed

+215
-8
lines changed

app/Resources/translations/messages.en.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@
288288
<source>post.deleted_successfully</source>
289289
<target>Post deleted successfully!</target>
290290
</trans-unit>
291+
<trans-unit id="post.search_for">
292+
<source>post.search_for</source>
293+
<target>Search for...</target>
294+
</trans-unit>
291295

292296
<trans-unit id="notification.comment_created">
293297
<source>notification.comment_created</source>

app/Resources/translations/messages.ru.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@
288288
<source>post.deleted_successfully</source>
289289
<target>Запись успешно удалена!</target>
290290
</trans-unit>
291+
<trans-unit id="post.search_for">
292+
<source>post.search_for</source>
293+
<target>Искать запись...</target>
294+
</trans-unit>
291295

292296
<trans-unit id="notification.comment_created">
293297
<source>notification.comment_created</source>

app/Resources/views/base.html.twig

+17-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<header>
2424
<div class="navbar navbar-default navbar-static-top" role="navigation">
2525
<div class="container">
26-
<div class="navbar-header">
26+
<div class="navbar-header col-md-3 col-lg-2">
2727
<a class="navbar-brand" href="{{ path('homepage') }}">
2828
Symfony Demo
2929
</a>
@@ -37,6 +37,13 @@
3737
<span class="icon-bar"></span>
3838
</button>
3939
</div>
40+
<div class="search-bar col-sm-5">
41+
<form action="{{ path('blog_search') }}" method="get">
42+
<div class="input-group-sm">
43+
<input name="q" type="text" class="form-control" placeholder="{{ 'post.search_for'|trans }}" autocomplete="off">
44+
</div>
45+
</form>
46+
</div>
4047
<div class="navbar-collapse collapse">
4148
<ul class="nav navbar-nav navbar-right">
4249

@@ -135,6 +142,15 @@
135142
<script src="{{ asset('build/manifest.js') }}"></script>
136143
<script src="{{ asset('build/js/common.js') }}"></script>
137144
<script src="{{ asset('build/js/app.js') }}"></script>
145+
<script>
146+
(function($) {
147+
$(function() {
148+
$('.search-bar input[name="q"]').instantSearch({
149+
noItemsFoundMessage: '{{ 'post.no_posts_found'|trans }}'
150+
});
151+
});
152+
})(window.jQuery);
153+
</script>
138154
{% endblock %}
139155

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

assets/js/app.js

+6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
// Exposes jQuery as a global variable
2+
global.$ = global.jQuery = require('jquery');
3+
14
// loads the Bootstrap jQuery plugins
25
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown.js';
36
import 'bootstrap-sass/assets/javascripts/bootstrap/modal.js';
47
import 'bootstrap-sass/assets/javascripts/bootstrap/transition.js';
58

69
// loads the code syntax highlighting library
710
import './highlight.js';
11+
12+
// loads the instant search library
13+
import './jquery.instantSearch.js';

assets/js/jquery.instantSearch.js

+100
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);

assets/scss/app.scss

+14
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ header .locales a {
6767
padding-right: 10px
6868
}
6969

70+
header .search-bar {
71+
padding: 0.8em 0;
72+
}
73+
74+
header .search-bar form {
75+
position: relative;
76+
}
77+
78+
header .search-preview {
79+
position: absolute;
80+
width: 100%;
81+
top: 100%;
82+
}
83+
7084
.body-container {
7185
flex: 1;
7286
/* needed to prevent pages with a very small height and browsers not supporting flex */

src/AppBundle/Controller/BlogController.php

+39
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
2424
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2525
use Symfony\Component\EventDispatcher\GenericEvent;
26+
use Symfony\Component\HttpFoundation\JsonResponse;
2627
use Symfony\Component\HttpFoundation\Request;
2728
use Symfony\Component\HttpFoundation\Response;
2829

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

src/AppBundle/Repository/PostRepository.php

+18
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,22 @@ private function createPaginator(Query $query, $page)
5959

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

web/build/css/app.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/build/js/app.js

+12-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)