* 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();
}
}
*/
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]);
+ }
}
// Search
Route::get('/search/all', 'SearchController@searchAll');
+ Route::get('/search/book/{bookId}', 'SearchController@searchBook');
// Other Pages
Route::get('/', 'HomeController@index');
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
$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);
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');
});
--- /dev/null
+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
-jQuery.fn.showSuccess = function(message) {
- var elem = $(this);
- var success = $('<div class="text-pos" style="display:none;"><i class="zmdi zmdi-check-circle"></i>'+message+'</div>');
- 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 = $('<div class="text-neg" style="display:none;"><i class="zmdi zmdi-alert-circle"></i>'+messages.join("\n")+'</div>');
- input.after(fail);
- fail.slideDown(400, function() {
- setTimeout(function() {fail.slideUp(400, function() {
- fail.remove();
- })}, 2000);
- });
- });
-
-};
-
-(function() {
-
- var ImageManager = new Vue({
-
- el: '#image-manager',
-
- data: {
- images: [],
- hasMore: false,
- page: 0,
- cClickTime: 0,
- selectedImage: false
- },
-
- created: function() {
- // Get initial images
- this.fetchData(this.page);
- },
- ready: function() {
- // Create dropzone
- this.setupDropZone();
+window.ImageManager = new Vue({
+
+ el: '#image-manager',
+
+ data: {
+ images: [],
+ hasMore: false,
+ page: 0,
+ cClickTime: 0,
+ selectedImage: false
+ },
+
+ created: function () {
+ // Get initial images
+ this.fetchData(this.page);
+ },
+
+ ready: function () {
+ // Create dropzone
+ this.setupDropZone();
+ },
+
+ 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++;
+ });
},
- 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';
+ },
- window.ImageManager = ImageManager;
+ 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');
+ })
+ }
+ }
-})();
\ No newline at end of file
+});
--- /dev/null
+
+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 = $('<div class="text-pos" style="display:none;"><i class="zmdi zmdi-check-circle"></i>' + message + '</div>');
+ 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 = $('<div class="text-neg" style="display:none;"><i class="zmdi zmdi-alert-circle"></i>' + messages.join("\n") + '</div>');
+ input.after(fail);
+ fail.slideDown(400, function () {
+ setTimeout(function () {
+ fail.slideUp(400, function () {
+ fail.remove();
+ })
+ }, 2000);
+ });
+ });
+
+};
\ No newline at end of file
}
}
+.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;
}
}
-
-
.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
}
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;
}
}
padding: $-l $-l $-l 0;
vertical-align: top;
line-height: 1;
+ &:hover {
+ color: #FFF;
+ text-decoration: none;
+ }
}
.page-title input {
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
<!-- Scripts -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
+ <script src="/js/jquery-extensions.js"></script>
<script src="/bower/bootstrap/dist/js/bootstrap.js"></script>
<script src="/bower/jquery-sortable/source/js/jquery-sortable.js"></script>
<script src="/bower/dropzone/dist/min/dropzone.min.js"></script>
<script src="/bower/vue/dist/vue.min.js"></script>
- <script>
- $.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;
- };
- });
- </script>
+ <script src="/bower/vue-resource/dist/vue-resource.min.js"></script>
@yield('head')
</head>
</div>
<div class="col-md-3 text-right">
<form action="/search/all" method="GET" class="search-box">
- <i class="zmdi zmdi-search"></i>
<input type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
+ <a onclick="$(this).closest('form').submit();"><i class="zmdi zmdi-search"></i></a>
</form>
</div>
<div class="col-md-6">
<form action="{{$book->getUrl()}}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
- <button type="submit" class="button neg">Confirm</button>
<a href="{{$book->getUrl()}}" class="button">Cancel</a>
+ <button type="submit" class="button neg">Confirm</button>
</form>
</div>
</div>
- <div class="container">
+ <div class="container" id="book-dashboard">
<div class="row">
<div class="col-md-7">
<h1>{{$book->name}}</h1>
- <p class="text-muted">{{$book->description}}</p>
-
- <div class="page-list">
- <hr>
- @if(count($book->children()) > 0)
- @foreach($book->children() as $childElement)
- <div class="book-child">
- <h3>
- <a href="{{ $childElement->getUrl() }}" class="{{ $childElement->getName() }}">
- <i class="zmdi {{ $childElement->isA('chapter') ? 'zmdi-collection-bookmark chapter-toggle':'zmdi-file-text'}}"></i>{{ $childElement->name }}
- </a>
- </h3>
- <p class="text-muted">
- {{$childElement->getExcerpt()}}
- </p>
-
- @if($childElement->isA('chapter') && count($childElement->pages) > 0)
- <div class="inset-list">
- @foreach($childElement->pages as $page)
- <h4><a href="{{$page->getUrl()}}"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h4>
- @endforeach
- </div>
- @endif
- </div>
+ <div class="book-content anim fadeIn" v-if="!searching">
+ <p class="text-muted">{{$book->description}}</p>
+
+ <div class="page-list">
+ <hr>
+ @if(count($book->children()) > 0)
+ @foreach($book->children() as $childElement)
+ <div class="book-child">
+ <h3>
+ <a href="{{ $childElement->getUrl() }}" class="{{ $childElement->getName() }}">
+ <i class="zmdi {{ $childElement->isA('chapter') ? 'zmdi-collection-bookmark chapter-toggle':'zmdi-file-text'}}"></i>{{ $childElement->name }}
+ </a>
+ </h3>
+ <p class="text-muted">
+ {{$childElement->getExcerpt()}}
+ </p>
+
+ @if($childElement->isA('chapter') && count($childElement->pages) > 0)
+ <div class="inset-list">
+ @foreach($childElement->pages as $page)
+ <h4><a href="{{$page->getUrl()}}"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h4>
+ @endforeach
+ </div>
+ @endif
+ </div>
+ <hr>
+ @endforeach
+ @else
+ <p class="text-muted">No pages or chapters have been created for this book.</p>
+ <p>
+ <a href="{{$book->getUrl() . '/page/create'}}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a>
+ <em class="text-muted">-or-</em>
+ <a href="{{$book->getUrl() . '/chapter/create'}}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>Add a chapter</a>
+ </p>
<hr>
- @endforeach
- @else
- <p class="text-muted">No pages or chapters have been created for this book.</p>
- <p>
- <a href="{{$book->getUrl() . '/page/create'}}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a>
- <em class="text-muted">-or-</em>
- <a href="{{$book->getUrl() . '/chapter/create'}}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>Add a chapter</a>
+ @endif
+ <p class="text-muted small">
+ Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by {{$book->createdBy->name}} @endif
+ <br>
+ Last Updated {{$book->updated_at->diffForHumans()}} @if($book->createdBy) by {{$book->updatedBy->name}} @endif
</p>
- <hr>
- @endif
+ </div>
+ </div>
+ <div class="search-results" v-if="searching">
+ <h3 class="text-muted">Search Results <a v-if="searching" v-on="click: clearSearch" class="text-small"><i class="zmdi zmdi-close"></i>Clear Search</a></h3>
+ <div v-html="searchResults"></div>
</div>
-
- <p class="text-muted small">
- Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by {{$book->createdBy->name}} @endif
- <br>
- Last Updated {{$book->updated_at->diffForHumans()}} @if($book->createdBy) by {{$book->updatedBy->name}} @endif
- </p>
</div>
<div class="col-md-4 col-md-offset-1">
- <div class="margin-top large"><br></div>
- <h3>Recent Activity</h3>
- @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
+ <div class="margin-top large"></div>
+ {{--<h3>Search This Book</h3>--}}
+ <div class="search-box">
+ <form v-on="submit: searchBook, input: checkSearchForm" v-el="form" action="/search/book/{{ $book->id }}">
+ {!! csrf_field() !!}
+ <input v-model="searchTerm" type="text" name="term" placeholder="Search This Book">
+ <button type="submit"><i class="zmdi zmdi-search"></i></button>
+ <button v-if="searching" v-on="click: clearSearch" type="button primary"><i class="zmdi zmdi-close"></i></button>
+ </form>
+ </div>
+ <div class="activity anim fadeIn">
+ <h3>Recent Activity</h3>
+ @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
+ </div>
</div>
</div>
</div>
});
</script>
+ <script src="/js/book-sidebar.js"></script>
+
@stop
\ No newline at end of file
<form action="{{$chapter->getUrl()}}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
- <a href="{{$chapter->getUrl()}}" class="button muted">Cancel</a>
+ <a href="{{$chapter->getUrl()}}" class="button primary">Cancel</a>
<button type="submit" class="button neg">Confirm</button>
</form>
</div>
<div class="page-content">
<h1>Delete Page</h1>
- <p>Are you sure you want to delete this page?</p>
+ <p class="text-neg">Are you sure you want to delete this page?</p>
<form action="{{$page->getUrl()}}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
- <a href="{{$page->getUrl()}}" class="button muted">Cancel</a>
+ <a href="{{$page->getUrl()}}" class="button primary">Cancel</a>
<button type="submit" class="button neg">Confirm</button>
</form>
</div>
</div>
</div>
<div class="col-md-9">
- <div class="page-content">
+ <div class="page-content anim fadeIn">
@include('pages/page-display')
<hr>
<p class="text-muted small">
@section('content')
- <div class="container">
+ <div class="container anim fadeIn">
<h1>Search Results <span class="text-muted">{{$searchTerm}}</span></h1>
--- /dev/null
+
+<div class="page-list">
+ @if(count($pages) > 0)
+ @foreach($pages as $page)
+ <div class="book-child anim searchResult">
+ <h3>
+ <a href="{{$page->getUrl() . '#' . $searchTerm}}" class="page">
+ <i class="zmdi zmdi-file-text"></i>{{$page->name}}
+ </a>
+ </h3>
+
+ <p class="text-muted">
+ {!! $page->searchSnippet !!}
+ </p>
+ <hr>
+ </div>
+ @endforeach
+ @else
+ <p class="text-muted">No pages matched this search</p>
+ @endif
+</div>
+
+@if(count($chapters) > 0)
+ <div class="page-list">
+ @foreach($chapters as $chapter)
+ <div class="book-child anim searchResult">
+ <h3>
+ <a href="{{$chapter->getUrl()}}" class="text-chapter">
+ <i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->name}}
+ </a>
+ </h3>
+
+ <p class="text-muted">
+ {!! $chapter->searchSnippet !!}
+ </p>
+ <hr>
+ </div>
+ @endforeach
+ </div>
+@endif
+