Skip to content

Add an instant post search #613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/Resources/translations/messages.en.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@
<source>menu.rss</source>
<target>Blog Posts RSS</target>
</trans-unit>
<trans-unit id="menu.search">
<source>menu.search</source>
<target>Search</target>
</trans-unit>

<trans-unit id="post.to_publish_a_comment">
<source>post.to_publish_a_comment</source>
Expand Down Expand Up @@ -288,6 +292,10 @@
<source>post.deleted_successfully</source>
<target>Post deleted successfully!</target>
</trans-unit>
<trans-unit id="post.search_for">
<source>post.search_for</source>
<target>Search for...</target>
</trans-unit>

<trans-unit id="notification.comment_created">
<source>notification.comment_created</source>
Expand Down
4 changes: 4 additions & 0 deletions app/Resources/translations/messages.ru.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@
<source>post.deleted_successfully</source>
<target>Запись успешно удалена!</target>
</trans-unit>
<trans-unit id="post.search_for">
<source>post.search_for</source>
<target>Искать запись...</target>
</trans-unit>

<trans-unit id="notification.comment_created">
<source>notification.comment_created</source>
Expand Down
6 changes: 5 additions & 1 deletion app/Resources/views/base.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<header>
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<div class="navbar-header col-md-3 col-lg-2">
<a class="navbar-brand" href="{{ path('homepage') }}">
Symfony Demo
</a>
Expand Down Expand Up @@ -64,6 +64,10 @@
</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
23 changes: 23 additions & 0 deletions app/Resources/views/blog/search.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends 'base.html.twig' %}

{% block javascripts %}
{{ parent() }}
<script src="{{ asset('build/js/search.js') }}"></script>
{% endblock %}

{% 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>
</div>
</form>

<div id="results">
</div>
{% endblock %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javiereguiluz, you forgot to add a widget with the Show source button.


{% block sidebar %}
{{ parent() }}

{{ show_source_code(_self) }}
{% endblock %}
93 changes: 93 additions & 0 deletions assets/js/jquery.instantSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* jQuery plugin for an instant searching.
*
* @author Oleg Voronkovich <[email protected]>
*/
(function($) {
$.fn.instantSearch = function(config) {
return this.each(function() {
initInstantSearch(this, $.extend(true, defaultConfig, config || {}));
});
};

var defaultConfig = {
minQueryLength: 2,
maxPreviewItems: 10,
previewDelay: 500,
noItemsFoundMessage: 'No results found.'
};

function debounce(fn, delay) {
var timer = null;
return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, 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);
});
}

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

$preview.append($result);
}

var noItemsFound = function() {
var $result = $('<div>').text(config.noItemsFoundMessage);

$preview.empty();
$preview.append($result);
}

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

if (query.length < config.minQueryLength) {
$preview.empty();
return;
}

$.getJSON($form.attr('action') + '?' + $form.serialize(), function(items) {
if (items.length === 0) {
noItemsFound();
return;
}

setPreviewItems(items);
});
}

$input.focusout(function(e) {
$preview.fadeOut();
});

$input.focusin(function(e) {
$preview.fadeIn();
updatePreview();
});

$input.keyup(debounce(updatePreview, config.previewDelay));
}
})(window.jQuery);
7 changes: 7 additions & 0 deletions assets/js/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import './jquery.instantSearch.js';

$(function() {
$('.search-field').instantSearch({
previewDelay: 100,
});
});
28 changes: 28 additions & 0 deletions src/AppBundle/Controller/BlogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

Expand Down Expand Up @@ -150,4 +151,31 @@ public function commentFormAction(Post $post)
'form' => $form->createView(),
]);
}

/**
* @Route("/search", name="blog_search")
* @Method("GET")
*
* @return Response|JsonResponse
*/
public function searchAction(Request $request)
{
if (!$request->isXmlHttpRequest()) {
return $this->render('blog/search.html.twig');
}

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

$results = [];
foreach ($posts as $post) {
$results[] = [
'title' => htmlspecialchars($post->getTitle()),
'summary' => htmlspecialchars($post->getSummary()),
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
];
}

return $this->json($results);
}
}
59 changes: 59 additions & 0 deletions src/AppBundle/Repository/PostRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,63 @@ private function createPaginator(Query $query, $page)

return $paginator;
}

/**
* @param string $rawQuery The search query as input by the user
* @param int $limit The maximum number of results returned
*
* @return array
*/
public function findBySearchQuery($rawQuery, $limit = Post::NUM_ITEMS)
{
$query = $this->sanitizeSearchQuery($rawQuery);
$searchTerms = $this->extractSearchTerms($query);

if (0 === count($searchTerms)) {
return [];
}

$queryBuilder = $this->createQueryBuilder('p');

foreach ($searchTerms as $key => $term) {
$queryBuilder
->orWhere('p.title LIKE :t_'.$key)
->setParameter('t_'.$key, '%'.$term.'%')
;
}

return $queryBuilder
->orderBy('p.publishedAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}

/**
* Removes all non-alphanumeric characters except whitespaces.
*
* @param string $query
*
* @return string
*/
private function sanitizeSearchQuery($query)
{
return preg_replace('/[^[:alnum:] ]/', '', trim(preg_replace('/[[:space:]]+/', ' ', $query)));
}

/**
* Splits the search query into terms and removes the ones which are irrelevant.
*
* @param string $searchQuery
*
* @return array
*/
private function extractSearchTerms($searchQuery)
{
$terms = array_unique(explode(' ', mb_strtolower($searchQuery)));

return array_filter($terms, function ($term) {
return 2 <= mb_strlen($term);
});
}
}
758 changes: 379 additions & 379 deletions web/build/js/admin.js

Large diffs are not rendered by default.

Loading