From d0c103eae0ec84426e48c9a20605bfa880db5c68 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sun, 11 Dec 2016 01:48:51 +0300 Subject: [PATCH] User search refactored, autocomplete added. bootstrap3-typeahead jQuery plugin added. --- app/config/config.yml | 5 +- .../Controller/MainController.php | 45 +++++++++++++++- .../Controller/UserController.php | 13 ----- .../PointToolsBundle/Form/UserSearchType.php | 39 ++++++++++++++ .../Repository/UserRepository.php | 27 +++++++++- .../Resources/config/routing.yml | 10 ++-- .../Resources/views/Main/index.html.twig | 52 +++++++++++++++---- web/js/bootstrap3-typeahead.min.js | 1 + 8 files changed, 162 insertions(+), 30 deletions(-) create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Form/UserSearchType.php create mode 100644 web/js/bootstrap3-typeahead.min.js diff --git a/app/config/config.yml b/app/config/config.yml index 79e1ae9..cafefe9 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -35,6 +35,9 @@ sensio_framework_extra: # Twig Configuration twig: + form: + resources: + - bootstrap_3_layout.html.twig debug: "%kernel.debug%" strict_variables: "%kernel.debug%" @@ -82,4 +85,4 @@ swiftmailer: knp_markdown: parser: - service: markdown.parser.point \ No newline at end of file + service: markdown.parser.point diff --git a/src/Skobkin/Bundle/PointToolsBundle/Controller/MainController.php b/src/Skobkin/Bundle/PointToolsBundle/Controller/MainController.php index f6a3d34..ed4e72f 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Controller/MainController.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Controller/MainController.php @@ -3,17 +3,42 @@ namespace Skobkin\Bundle\PointToolsBundle\Controller; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\QueryBuilder; +use Skobkin\Bundle\PointToolsBundle\Form\UserSearchType; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Form\FormError; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; class MainController extends Controller { - public function indexAction() + public function indexAction(Request $request) { /** @var EntityManager $em */ $em = $this->getDoctrine()->getManager(); + $form = $this->createForm( + new UserSearchType(), + null, + [ + 'action' => $this->generateUrl('index'), + 'method' => 'POST', + ] + ); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $login = $form->get('login')->getData(); + + if (null !== $user = $em->getRepository('SkobkinPointToolsBundle:User')->findOneBy(['login' => $login])) { + return $this->redirectToRoute('user_show', ['login' => $login]); + } + + $form->get('login')->addError(new FormError('Login not found')); + } + return $this->render('SkobkinPointToolsBundle:Main:index.html.twig', [ + 'form' => $form->createView(), 'users_count' => $em->getRepository('SkobkinPointToolsBundle:User')->getUsersCount(), 'subscribers_count' => $em->getRepository('SkobkinPointToolsBundle:Subscription')->getUserSubscribersCountById($this->container->getParameter('point_id')), 'events_count' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastDayEventsCount(), @@ -21,4 +46,20 @@ class MainController extends Controller 'last_events' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastSubscriptionEvents(10), ]); } + + public function searchUserAjaxAction($login) + { + $em = $this->getDoctrine()->getManager(); + + $result = []; + + foreach ($em->getRepository('SkobkinPointToolsBundle:User')->findUsersLikeLogin($login) as $user) { + $result[] = [ + 'login' => $user->getLogin(), + 'name' => $user->getName(), + ]; + } + + return new JsonResponse($result); + } } diff --git a/src/Skobkin/Bundle/PointToolsBundle/Controller/UserController.php b/src/Skobkin/Bundle/PointToolsBundle/Controller/UserController.php index e328161..b1af69b 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Controller/UserController.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Controller/UserController.php @@ -50,19 +50,6 @@ class UserController extends Controller ]); } - /** - * @param Request $request - */ - public function searchUserAction(Request $request) - { - $login = $request->request->get('login'); - - if (!$login) { - return $this->redirectToRoute('index'); - } - return $this->redirectToRoute('user_show', ['login' => $login]); - } - /** * @param TopUserDTO[] $topUsers * @return Highchart diff --git a/src/Skobkin/Bundle/PointToolsBundle/Form/UserSearchType.php b/src/Skobkin/Bundle/PointToolsBundle/Form/UserSearchType.php new file mode 100644 index 0000000..48b6871 --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Form/UserSearchType.php @@ -0,0 +1,39 @@ +add('login') + ; + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => 'Skobkin\Bundle\PointToolsBundle\Entity\User', + ]); + } + + /** + * @return string + */ + public function getName() + { + return 'skobkin_bundle_pointtoolsbundle_user_search'; + } +} diff --git a/src/Skobkin/Bundle/PointToolsBundle/Repository/UserRepository.php b/src/Skobkin/Bundle/PointToolsBundle/Repository/UserRepository.php index 2565fff..ba1bed4 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Repository/UserRepository.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Repository/UserRepository.php @@ -12,7 +12,7 @@ class UserRepository extends EntityRepository * Case-insensitive user search * * @param string $login - * @return User[] + * @return User|null * @throws \Doctrine\ORM\NonUniqueResultException */ public function findUserByLogin($login) @@ -28,6 +28,31 @@ class UserRepository extends EntityRepository ; } + /** + * Case insensitive user LIKE %login% search + * + * @param string $login + * + * @return User[] + */ + public function findUsersLikeLogin($login) + { + if (empty($login)) { + return []; + } + + $qb = $this->createQueryBuilder('u'); + + return $qb + ->where('u.login LIKE :login') + ->orderBy('u.login', 'ASC') + ->setMaxResults(10) + ->setParameter('login', '%'.strtolower($login).'%') + ->getQuery() + ->getResult() + ; + } + /** * @return integer */ diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/config/routing.yml b/src/Skobkin/Bundle/PointToolsBundle/Resources/config/routing.yml index a5c24e8..74cd91d 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Resources/config/routing.yml +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/config/routing.yml @@ -2,10 +2,12 @@ index: path: / defaults: { _controller: SkobkinPointToolsBundle:Main:index } -user_search: - path: /user/search - defaults: { _controller: SkobkinPointToolsBundle:User:searchUser } - methods: [POST] +user_search_ajax: + path: /ajax/users/search/{login} + defaults: { _controller: SkobkinPointToolsBundle:Main:searchUserAjax, _format: json } + requirements: + login: "[\w-]*" + _format: json user_show: path: /user/{login} diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Main/index.html.twig b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Main/index.html.twig index 5274662..95b922f 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Main/index.html.twig +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/views/Main/index.html.twig @@ -1,18 +1,52 @@ {% extends "::base.html.twig" %} +{% block head_js %} + {{ parent() }} + +{% endblock %} + {% block content %}
- {# @todo rewrite to Symfony forms #} -
+ {{ form_start(form, {'attr': {'class': 'form-inline'} }) }}
- -
-
@
- -
+ {{ form_errors(form.login) }} + {{ form_widget(form.login, {'attr': {'autocomplete': 'off'}}) }} + + + +
- -
+ {{ form_end(form) }}
diff --git a/web/js/bootstrap3-typeahead.min.js b/web/js/bootstrap3-typeahead.min.js new file mode 100644 index 0000000..93d4c92 --- /dev/null +++ b/web/js/bootstrap3-typeahead.min.js @@ -0,0 +1 @@ +(function(root,factory){"use strict";if(typeof module!=="undefined"&&module.exports){module.exports=factory(require("jquery"))}else if(typeof define==="function"&&define.amd){define(["jquery"],function($){return factory($)})}else{factory(root.jQuery)}})(this,function($){"use strict";var Typeahead=function(element,options){this.$element=$(element);this.options=$.extend({},$.fn.typeahead.defaults,options);this.matcher=this.options.matcher||this.matcher;this.sorter=this.options.sorter||this.sorter;this.select=this.options.select||this.select;this.autoSelect=typeof this.options.autoSelect=="boolean"?this.options.autoSelect:true;this.highlighter=this.options.highlighter||this.highlighter;this.render=this.options.render||this.render;this.updater=this.options.updater||this.updater;this.displayText=this.options.displayText||this.displayText;this.source=this.options.source;this.delay=this.options.delay;this.$menu=$(this.options.menu);this.$appendTo=this.options.appendTo?$(this.options.appendTo):null;this.fitToElement=typeof this.options.fitToElement=="boolean"?this.options.fitToElement:false;this.shown=false;this.listen();this.showHintOnFocus=typeof this.options.showHintOnFocus=="boolean"||this.options.showHintOnFocus==="all"?this.options.showHintOnFocus:false;this.afterSelect=this.options.afterSelect;this.addItem=false;this.value=this.$element.val()||this.$element.text()};Typeahead.prototype={constructor:Typeahead,select:function(){var val=this.$menu.find(".active").data("value");this.$element.data("active",val);if(this.autoSelect||val){var newVal=this.updater(val);if(!newVal){newVal=""}this.$element.val(this.displayText(newVal)||newVal).text(this.displayText(newVal)||newVal).change();this.afterSelect(newVal)}return this.hide()},updater:function(item){return item},setSource:function(source){this.source=source},show:function(){var pos=$.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});var scrollHeight=typeof this.options.scrollHeight=="function"?this.options.scrollHeight.call():this.options.scrollHeight;var element;if(this.shown){element=this.$menu}else if(this.$appendTo){element=this.$menu.appendTo(this.$appendTo);this.hasSameParent=this.$appendTo.is(this.$element.parent())}else{element=this.$menu.insertAfter(this.$element);this.hasSameParent=true}if(!this.hasSameParent){element.css("position","fixed");var offset=this.$element.offset();pos.top=offset.top;pos.left=offset.left}var dropup=$(element).parent().hasClass("dropup");var newTop=dropup?"auto":pos.top+pos.height+scrollHeight;var right=$(element).hasClass("dropdown-menu-right");var newLeft=right?"auto":pos.left;element.css({top:newTop,left:newLeft}).show();if(this.options.fitToElement===true){element.css("width",this.$element.outerWidth()+"px")}this.shown=true;return this},hide:function(){this.$menu.hide();this.shown=false;return this},lookup:function(query){var items;if(typeof query!="undefined"&&query!==null){this.query=query}else{this.query=this.$element.val()||this.$element.text()||""}if(this.query.length0){this.$element.data("active",items[0])}else{this.$element.data("active",null)}if(this.options.addItem){items.push(this.options.addItem)}if(this.options.items=="all"){return this.render(items).show()}else{return this.render(items.slice(0,this.options.items)).show()}},matcher:function(item){var it=this.displayText(item);return~it.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(items){var beginswith=[];var caseSensitive=[];var caseInsensitive=[];var item;while(item=items.shift()){var it=this.displayText(item);if(!it.toLowerCase().indexOf(this.query.toLowerCase()))beginswith.push(item);else if(~it.indexOf(this.query))caseSensitive.push(item);else caseInsensitive.push(item)}return beginswith.concat(caseSensitive,caseInsensitive)},highlighter:function(item){var html=$("
");var query=this.query;var i=item.toLowerCase().indexOf(query.toLowerCase());var len=query.length;var leftPart;var middlePart;var rightPart;var strong;if(len===0){return html.text(item).html()}while(i>-1){leftPart=item.substr(0,i);middlePart=item.substr(i,len);rightPart=item.substr(i+len);strong=$("").text(middlePart);html.append(document.createTextNode(leftPart)).append(strong);item=rightPart;i=item.toLowerCase().indexOf(query.toLowerCase())}return html.append(document.createTextNode(item)).html()},render:function(items){var that=this;var self=this;var activeFound=false;var data=[];var _category=that.options.separator;$.each(items,function(key,value){if(key>0&&value[_category]!==items[key-1][_category]){data.push({__type:"divider"})}if(value[_category]&&(key===0||value[_category]!==items[key-1][_category])){data.push({__type:"category",name:value[_category]})}data.push(value)});items=$(data).map(function(i,item){if((item.__type||false)=="category"){return $(that.options.headerHtml).text(item.name)[0]}if((item.__type||false)=="divider"){return $(that.options.headerDivider)[0]}var text=self.displayText(item);i=$(that.options.item).data("value",item);i.find("a").html(that.highlighter(text,item));if(text==self.$element.val()){i.addClass("active");self.$element.data("active",item);activeFound=true}return i[0]});if(this.autoSelect&&!activeFound){items.filter(":not(.dropdown-header)").first().addClass("active");this.$element.data("active",items.first().data("value"))}this.$menu.html(items);return this},displayText:function(item){return typeof item!=="undefined"&&typeof item.name!="undefined"&&item.name||item},next:function(event){var active=this.$menu.find(".active").removeClass("active");var next=active.next();if(!next.length){next=$(this.$menu.find("li")[0])}next.addClass("active")},prev:function(event){var active=this.$menu.find(".active").removeClass("active");var prev=active.prev();if(!prev.length){prev=this.$menu.find("li").last()}prev.addClass("active")},listen:function(){this.$element.on("focus",$.proxy(this.focus,this)).on("blur",$.proxy(this.blur,this)).on("keypress",$.proxy(this.keypress,this)).on("input",$.proxy(this.input,this)).on("keyup",$.proxy(this.keyup,this));if(this.eventSupported("keydown")){this.$element.on("keydown",$.proxy(this.keydown,this))}this.$menu.on("click",$.proxy(this.click,this)).on("mouseenter","li",$.proxy(this.mouseenter,this)).on("mouseleave","li",$.proxy(this.mouseleave,this)).on("mousedown",$.proxy(this.mousedown,this))},destroy:function(){this.$element.data("typeahead",null);this.$element.data("active",null);this.$element.off("focus").off("blur").off("keypress").off("input").off("keyup");if(this.eventSupported("keydown")){this.$element.off("keydown")}this.$menu.remove();this.destroyed=true},eventSupported:function(eventName){var isSupported=eventName in this.$element;if(!isSupported){this.$element.setAttribute(eventName,"return;");isSupported=typeof this.$element[eventName]==="function"}return isSupported},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:if(e.shiftKey)return;e.preventDefault();this.prev();break;case 40:if(e.shiftKey)return;e.preventDefault();this.next();break}},keydown:function(e){this.suppressKeyPressRepeat=~$.inArray(e.keyCode,[40,38,9,13,27]);if(!this.shown&&e.keyCode==40){this.lookup()}else{this.move(e)}},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},input:function(e){var currentValue=this.$element.val()||this.$element.text();if(this.value!==currentValue){this.value=currentValue;this.lookup()}},keyup:function(e){if(this.destroyed){return}switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break}},focus:function(e){if(!this.focused){this.focused=true;if(this.options.showHintOnFocus&&this.skipShowHintOnFocus!==true){if(this.options.showHintOnFocus==="all"){this.lookup("")}else{this.lookup()}}}if(this.skipShowHintOnFocus){this.skipShowHintOnFocus=false}},blur:function(e){if(!this.mousedover&&!this.mouseddown&&this.shown){this.hide();this.focused=false}else if(this.mouseddown){this.skipShowHintOnFocus=true;this.$element.focus();this.mouseddown=false}},click:function(e){e.preventDefault();this.skipShowHintOnFocus=true;this.select();this.$element.focus();this.hide()},mouseenter:function(e){this.mousedover=true;this.$menu.find(".active").removeClass("active");$(e.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=false;if(!this.focused&&this.shown)this.hide()},mousedown:function(e){this.mouseddown=true;this.$menu.one("mouseup",function(e){this.mouseddown=false}.bind(this))}};var old=$.fn.typeahead;$.fn.typeahead=function(option){var arg=arguments;if(typeof option=="string"&&option=="getActive"){return this.data("active")}return this.each(function(){var $this=$(this);var data=$this.data("typeahead");var options=typeof option=="object"&&option;if(!data)$this.data("typeahead",data=new Typeahead(this,options));if(typeof option=="string"&&data[option]){if(arg.length>1){data[option].apply(data,Array.prototype.slice.call(arg,1))}else{data[option]()}}})};$.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1,scrollHeight:0,autoSelect:true,afterSelect:$.noop,addItem:false,delay:0,separator:"category",headerHtml:'',headerDivider:''};$.fn.typeahead.Constructor=Typeahead;$.fn.typeahead.noConflict=function(){$.fn.typeahead=old;return this};$(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(e){var $this=$(this);if($this.data("typeahead"))return;$this.typeahead($this.data())})});