From 03f5f9e9b95542fc2b116844ede0492c833fa83a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 1 Sep 2015 15:35:11 +0100 Subject: [PATCH] Added AJAX-based search to books, Fixes #15 --- app/Entity.php | 11 +- app/Http/Controllers/SearchController.php | 22 +- app/Http/routes.php | 1 + app/Repos/ChapterRepo.php | 4 +- app/Repos/PageRepo.php | 4 +- gulpfile.js | 2 + resources/assets/js/book-sidebar.js | 31 +++ resources/assets/js/image-manager.js | 238 ++++++++++------------ resources/assets/js/jquery-extensions.js | 43 ++++ resources/assets/sass/_animations.scss | 20 ++ resources/assets/sass/_forms.scss | 21 +- resources/assets/sass/styles.scss | 27 ++- resources/views/base.blade.php | 18 +- resources/views/books/delete.blade.php | 2 +- resources/views/books/show.blade.php | 104 ++++++---- resources/views/chapters/delete.blade.php | 2 +- resources/views/pages/delete.blade.php | 4 +- resources/views/pages/show.blade.php | 2 +- resources/views/search/all.blade.php | 2 +- resources/views/search/book.blade.php | 41 ++++ 20 files changed, 385 insertions(+), 214 deletions(-) create mode 100644 resources/assets/js/book-sidebar.js create mode 100644 resources/assets/js/jquery-extensions.js create mode 100644 resources/views/search/book.blade.php diff --git a/app/Entity.php b/app/Entity.php index 0fd5d1e12..23b61e417 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -69,16 +69,21 @@ class Entity extends Model * Perform a full-text search on this entity. * @param string[] $fieldsToSearch * @param string[] $terms + * @param string[] array $wheres * @return mixed */ - public static function fullTextSearch($fieldsToSearch, $terms) + public static function fullTextSearch($fieldsToSearch, $terms, $wheres = []) { $termString = ''; - foreach($terms as $term) { + foreach ($terms as $term) { $termString .= $term . '* '; } $fields = implode(',', $fieldsToSearch); - return static::whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString])->get(); + $search = static::whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); + foreach ($wheres as $whereTerm) { + $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); + } + return $search->get(); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 074a4c777..360e1139e 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -38,15 +38,33 @@ class SearchController extends Controller */ public function searchAll(Request $request) { - if(!$request->has('term')) { + if (!$request->has('term')) { return redirect()->back(); } $searchTerm = $request->get('term'); $pages = $this->pageRepo->getBySearch($searchTerm); $books = $this->bookRepo->getBySearch($searchTerm); $chapters = $this->chapterRepo->getBySearch($searchTerm); - return view('search/all', ['pages' => $pages, 'books'=>$books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); + return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); } + /** + * Searches all entities within a book. + * @param Request $request + * @param integer $bookId + * @return \Illuminate\View\View + * @internal param string $searchTerm + */ + public function searchBook(Request $request, $bookId) + { + if (!$request->has('term')) { + return redirect()->back(); + } + $searchTerm = $request->get('term'); + $whereTerm = [['book_id', '=', $bookId]]; + $pages = $this->pageRepo->getBySearch($searchTerm, $whereTerm); + $chapters = $this->chapterRepo->getBySearch($searchTerm, $whereTerm); + return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index c7ff66f21..b4e515f3e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -66,6 +66,7 @@ Route::group(['middleware' => 'auth'], function () { // Search Route::get('/search/all', 'SearchController@searchAll'); + Route::get('/search/book/{bookId}', 'SearchController@searchBook'); // Other Pages Route::get('/', 'HomeController@index'); diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index 0dcaa5fbd..b423ee8ba 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -67,10 +67,10 @@ class ChapterRepo return $slug; } - public function getBySearch($term) + public function getBySearch($term, $whereTerms = []) { $terms = explode(' ', preg_quote(trim($term))); - $chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms); + $chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms); $words = join('|', $terms); foreach ($chapters as $chapter) { //highlight diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 51a3e8ce9..1d2e08580 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -59,10 +59,10 @@ class PageRepo $page->delete(); } - public function getBySearch($term) + public function getBySearch($term, $whereTerms = []) { $terms = explode(' ', preg_quote(trim($term))); - $pages = $this->page->fullTextSearch(['name', 'text'], $terms); + $pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms); // Add highlights to page text. $words = join('|', $terms); diff --git a/gulpfile.js b/gulpfile.js index f6a86d4c9..36693223d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -14,4 +14,6 @@ var elixir = require('laravel-elixir'); elixir(function(mix) { mix.sass('styles.scss'); mix.scripts('image-manager.js', 'public/js/image-manager.js'); + mix.scripts('book-sidebar.js', 'public/js/book-sidebar.js'); + mix.scripts('jquery-extensions.js', 'public/js/jquery-extensions.js'); }); diff --git a/resources/assets/js/book-sidebar.js b/resources/assets/js/book-sidebar.js new file mode 100644 index 000000000..1315843ab --- /dev/null +++ b/resources/assets/js/book-sidebar.js @@ -0,0 +1,31 @@ +var bookDashboard = new Vue({ + el: '#book-dashboard', + data: { + searching: false, + searchTerm: '', + searchResults: '' + }, + methods: { + searchBook: function (e) { + e.preventDefault(); + var term = this.searchTerm; + if (term.length == 0) return; + this.searching = true; + this.searchResults = ''; + var searchUrl = this.$$.form.getAttribute('action'); + searchUrl += '?term=' + encodeURIComponent(term); + this.$http.get(searchUrl, function (data) { + this.$set('searchResults', data); + }); + }, + checkSearchForm: function (e) { + if (this.searchTerm.length < 1) { + this.searching = false; + } + }, + clearSearch: function(e) { + this.searching = false; + this.searchTerm = ''; + } + } +}); \ No newline at end of file diff --git a/resources/assets/js/image-manager.js b/resources/assets/js/image-manager.js index 4f8695ff8..73a7fb4a2 100644 --- a/resources/assets/js/image-manager.js +++ b/resources/assets/js/image-manager.js @@ -1,154 +1,122 @@ -jQuery.fn.showSuccess = function(message) { - var elem = $(this); - var success = $(''); - elem.after(success); - success.slideDown(400, function() { - setTimeout(function() {success.slideUp(400, function() { - success.remove(); - })}, 2000); - }); -}; -jQuery.fn.showFailure = function(messageMap) { - var elem = $(this); - $.each(messageMap, function(key, messages) { - var input = elem.find('[name="'+key+'"]').last(); - var fail = $(''); - input.after(fail); - fail.slideDown(400, function() { - setTimeout(function() {fail.slideUp(400, function() { - fail.remove(); - })}, 2000); - }); - }); +window.ImageManager = new Vue({ -}; + el: '#image-manager', -(function() { + data: { + images: [], + hasMore: false, + page: 0, + cClickTime: 0, + selectedImage: false + }, - var ImageManager = new Vue({ + created: function () { + // Get initial images + this.fetchData(this.page); + }, - el: '#image-manager', + ready: function () { + // Create dropzone + this.setupDropZone(); + }, - data: { - images: [], - hasMore: false, - page: 0, - cClickTime: 0, - selectedImage: false + methods: { + fetchData: function () { + var _this = this; + this.$http.get('/images/all/' + _this.page, function (data) { + _this.images = _this.images.concat(data.images); + _this.hasMore = data.hasMore; + _this.page++; + }); }, - created: function() { - // Get initial images - this.fetchData(this.page); - }, - - ready: function() { - // Create dropzone - this.setupDropZone(); - }, - - methods: { - fetchData: function() { - var _this = this; - $.getJSON('/images/all/' + _this.page, function(data) { - _this.images = _this.images.concat(data.images); - _this.hasMore = data.hasMore; - _this.page++; - }); - }, - - setupDropZone: function() { - var _this = this; - var dropZone = new Dropzone(_this.$$.dropZone, { - url: '/upload/image', - init: function() { - var dz = this; - this.on("sending", function(file, xhr, data) { - data.append("_token", document.querySelector('meta[name=token]').getAttribute('content')); + setupDropZone: function () { + var _this = this; + var dropZone = new Dropzone(_this.$$.dropZone, { + url: '/upload/image', + init: function () { + var dz = this; + this.on("sending", function (file, xhr, data) { + data.append("_token", document.querySelector('meta[name=token]').getAttribute('content')); + }); + this.on("success", function (file, data) { + _this.images.unshift(data); + $(file.previewElement).fadeOut(400, function () { + dz.removeFile(file); }); - this.on("success", function(file, data) { - _this.images.unshift(data); - $(file.previewElement).fadeOut(400, function() { - dz.removeFile(file); - }); - }); - } - }); - }, - - imageClick: function(image) { - var dblClickTime = 380; - var cTime = (new Date()).getTime(); - var timeDiff = cTime - this.cClickTime; - if(this.cClickTime !== 0 && timeDiff < dblClickTime && this.selectedImage === image) { - // DoubleClick - if(this.callback) { - this.callback(image); - } - this.hide(); - } else { - this.selectedImage = (this.selectedImage===image) ? false : image; + }); } - this.cClickTime = cTime; - }, + }); + }, - selectButtonClick: function() { - if(this.callback) { - this.callback(this.selectedImage); + imageClick: function (image) { + var dblClickTime = 380; + var cTime = (new Date()).getTime(); + var timeDiff = cTime - this.cClickTime; + if (this.cClickTime !== 0 && timeDiff < dblClickTime && this.selectedImage === image) { + // DoubleClick + if (this.callback) { + this.callback(image); } this.hide(); - }, - - show: function(callback) { - this.callback = callback; - this.$$.overlay.style.display = 'block'; - }, - - overlayClick: function(e) { - if(e.target.className==='overlay') { - this.hide(); - } - }, - - hide: function() { - this.$$.overlay.style.display = 'none'; - }, - - saveImageDetails: function(e) { - e.preventDefault(); - var _this = this; - var form = $(_this.$$.imageForm); - $.ajax('/images/update/' + _this.selectedImage.id, { - method: 'PUT', - data: form.serialize() - }).done(function() { - form.showSuccess('Image name updated'); - }).fail(function(jqXHR) { - form.showFailure(jqXHR.responseJSON); - }) - }, - - deleteImage: function(e) { - e.preventDefault(); - var _this = this; - var form = $(_this.$$.imageDeleteForm); - $.ajax('/images/' + _this.selectedImage.id, { - method: 'DELETE', - data: form.serialize() - }).done(function() { - _this.images.splice(_this.images.indexOf(_this.selectedImage), 1); - _this.selectedImage = false; - $(_this.$$.imageTitle).showSuccess('Image Deleted'); - }) + } else { + this.selectedImage = (this.selectedImage === image) ? false : image; } + this.cClickTime = cTime; + }, + selectButtonClick: function () { + if (this.callback) { + this.callback(this.selectedImage); + } + this.hide(); + }, + + show: function (callback) { + this.callback = callback; + this.$$.overlay.style.display = 'block'; + }, + + overlayClick: function (e) { + if (e.target.className === 'overlay') { + this.hide(); + } + }, + + hide: function () { + this.$$.overlay.style.display = 'none'; + }, + + saveImageDetails: function (e) { + e.preventDefault(); + var _this = this; + var form = $(_this.$$.imageForm); + $.ajax('/images/update/' + _this.selectedImage.id, { + method: 'PUT', + data: form.serialize() + }).done(function () { + form.showSuccess('Image name updated'); + }).fail(function (jqXHR) { + form.showFailure(jqXHR.responseJSON); + }) + }, + + deleteImage: function (e) { + e.preventDefault(); + var _this = this; + var form = $(_this.$$.imageDeleteForm); + $.ajax('/images/' + _this.selectedImage.id, { + method: 'DELETE', + data: form.serialize() + }).done(function () { + _this.images.splice(_this.images.indexOf(_this.selectedImage), 1); + _this.selectedImage = false; + $(_this.$$.imageTitle).showSuccess('Image Deleted'); + }) } - }); + } - window.ImageManager = ImageManager; - - -})(); \ No newline at end of file +}); diff --git a/resources/assets/js/jquery-extensions.js b/resources/assets/js/jquery-extensions.js new file mode 100644 index 000000000..c28c5d150 --- /dev/null +++ b/resources/assets/js/jquery-extensions.js @@ -0,0 +1,43 @@ + +jQuery.fn.smoothScrollTo = function() { + if(this.length === 0) return; + $('body').animate({ + scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin + }, 800); // Adjust to change animations speed (ms) + return this; +}; +$.expr[":"].contains = $.expr.createPseudo(function(arg) { + return function( elem ) { + return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; + }; +}); + +jQuery.fn.showSuccess = function (message) { + var elem = $(this); + var success = $(''); + elem.after(success); + success.slideDown(400, function () { + setTimeout(function () { + success.slideUp(400, function () { + success.remove(); + }) + }, 2000); + }); +}; + +jQuery.fn.showFailure = function (messageMap) { + var elem = $(this); + $.each(messageMap, function (key, messages) { + var input = elem.find('[name="' + key + '"]').last(); + var fail = $(''); + input.after(fail); + fail.slideDown(400, function () { + setTimeout(function () { + fail.slideUp(400, function () { + fail.remove(); + }) + }, 2000); + }); + }); + +}; \ No newline at end of file diff --git a/resources/assets/sass/_animations.scss b/resources/assets/sass/_animations.scss index 8d3336489..cc1d42be9 100644 --- a/resources/assets/sass/_animations.scss +++ b/resources/assets/sass/_animations.scss @@ -16,6 +16,26 @@ } } +.anim.searchResult { + opacity: 0; + transform: translate3d(580px, 0, 0); + animation-name: searchResult; + animation-duration: 220ms; + animation-fill-mode: forwards; + animation-timing-function: cubic-bezier(.62,.28,.23,.99); +} + +@keyframes searchResult { + 0% { + opacity: 0; + transform: translate3d(400px, 0, 0); + } + 100% { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + .anim.notification { transform: translate3d(580px, 0, 0); animation-name: notification; diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 49716154f..9536b93f3 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -90,11 +90,28 @@ input[type="text"], input[type="number"], input[type="email"], input[type="searc } } - - .description-input textarea { @extend .inline-input-style; font-size: $fs-m; color: #666; width: 100%; +} + +.search-box { + button { + background-color: transparent; + border: none; + color: $primary; + padding: 0; + margin: 0; + cursor: pointer; + margin-left: $-s; + } + button[type="submit"] { + margin-left: -$-l; + } + input { + padding-right: $-l; + width: 300px; + } } \ No newline at end of file diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 0101aed9f..665315287 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -57,19 +57,25 @@ header { } form.search-box { - padding-top: $-l *0.9; + margin-top: $-l *0.9; display: inline-block; + position: relative; input { background-color: transparent; border-radius: 0; border: none; border-bottom: 2px solid #EEE; color: #EEE; - padding-left: $-l; + padding-right: $-l; outline: 0; } - i { - margin-right: -$-l; + a { + vertical-align: top; + margin-left: -$-l; + color: #FFF; + top: 0; + display: inline-block; + position: absolute; } } @@ -121,6 +127,10 @@ body.flexbox { padding: $-l $-l $-l 0; vertical-align: top; line-height: 1; + &:hover { + color: #FFF; + text-decoration: none; + } } .page-title input { @@ -537,4 +547,13 @@ ul.dropdown { li.border-bottom { border-bottom: 1px solid #DDD; } +} + +.search-results > h3 a { + font-size: 0.66em; + color: $primary; + padding-left: $-m; + i { + padding-right: $-s; + } } \ No newline at end of file diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index f20126b37..3c692317f 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -14,24 +14,12 @@ + - + @yield('head') @@ -57,8 +45,8 @@
diff --git a/resources/views/books/delete.blade.php b/resources/views/books/delete.blade.php index 30965071c..6ec8cdb3b 100644 --- a/resources/views/books/delete.blade.php +++ b/resources/views/books/delete.blade.php @@ -10,8 +10,8 @@
{!! csrf_field() !!} - Cancel +
diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index b44cd8505..6e1aaf7b4 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -27,61 +27,77 @@ -
+

{{$book->name}}

-

{{$book->description}}

+
+

{{$book->description}}

-
-
- @if(count($book->children()) > 0) - @foreach($book->children() as $childElement) -
-

- - {{ $childElement->name }} - -

-

- {{$childElement->getExcerpt()}} -

- - @if($childElement->isA('chapter') && count($childElement->pages) > 0) -
- @foreach($childElement->pages as $page) -

{{$page->name}}

- @endforeach -
- @endif -
-
- @endforeach - @else -

No pages or chapters have been created for this book.

-

- Create a new page -   -or-    - Add a chapter -

+

- @endif -
+ @if(count($book->children()) > 0) + @foreach($book->children() as $childElement) +
+

+ + {{ $childElement->name }} + +

+

+ {{$childElement->getExcerpt()}} +

-

- Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by {{$book->createdBy->name}} @endif -
- Last Updated {{$book->updated_at->diffForHumans()}} @if($book->createdBy) by {{$book->updatedBy->name}} @endif -

+ @if($childElement->isA('chapter') && count($childElement->pages) > 0) +
+ @foreach($childElement->pages as $page) +

{{$page->name}}

+ @endforeach +
+ @endif +
+
+ @endforeach + @else +

No pages or chapters have been created for this book.

+

+ Create a new page +   -or-    + Add a chapter +

+
+ @endif +

+ Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by {{$book->createdBy->name}} @endif +
+ Last Updated {{$book->updated_at->diffForHumans()}} @if($book->createdBy) by {{$book->updatedBy->name}} @endif +

+
+
+
+

Search Results Clear Search

+
+
-

-

Recent Activity

- @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)]) +
+ {{--

Search This Book

--}} + +
+

Recent Activity

+ @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)]) +
@@ -99,4 +115,6 @@ }); + + @stop \ No newline at end of file diff --git a/resources/views/chapters/delete.blade.php b/resources/views/chapters/delete.blade.php index 1607e7b6a..512535776 100644 --- a/resources/views/chapters/delete.blade.php +++ b/resources/views/chapters/delete.blade.php @@ -11,7 +11,7 @@
{!! csrf_field() !!} - Cancel + Cancel
diff --git a/resources/views/pages/delete.blade.php b/resources/views/pages/delete.blade.php index 3d199cd1d..45d87f151 100644 --- a/resources/views/pages/delete.blade.php +++ b/resources/views/pages/delete.blade.php @@ -4,12 +4,12 @@

Delete Page

-

Are you sure you want to delete this page?

+

Are you sure you want to delete this page?

{!! csrf_field() !!} - Cancel + Cancel
diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 46bd1714c..51f615669 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -44,7 +44,7 @@
-
+
@include('pages/page-display')

diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php index f2c922de2..3cb61b761 100644 --- a/resources/views/search/all.blade.php +++ b/resources/views/search/all.blade.php @@ -2,7 +2,7 @@ @section('content') -

+

Search Results    {{$searchTerm}}

diff --git a/resources/views/search/book.blade.php b/resources/views/search/book.blade.php new file mode 100644 index 000000000..329c3889e --- /dev/null +++ b/resources/views/search/book.blade.php @@ -0,0 +1,41 @@ + +
+ @if(count($pages) > 0) + @foreach($pages as $page) +
+

+ + {{$page->name}} + +

+ +

+ {!! $page->searchSnippet !!} +

+
+
+ @endforeach + @else +

No pages matched this search

+ @endif +
+ +@if(count($chapters) > 0) +
+ @foreach($chapters as $chapter) +
+

+ + {{$chapter->name}} + +

+ +

+ {!! $chapter->searchSnippet !!} +

+
+
+ @endforeach +
+@endif +