return $this->hasMany(Page::class);
}
+ /**
+ * Get the direct child pages of this book.
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function directPages()
+ {
+ return $this->pages()->where('chapter_id', '=', '0');
+ }
+
/**
* Get all chapters within this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
+ /**
+ * Get the direct children of a book.
+ * @param Book $book
+ * @return \Illuminate\Database\Eloquent\Collection
+ */
+ public function getBookDirectChildren(Book $book)
+ {
+ $pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
+ $chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
+ return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
+ }
+
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
use BookStack\Actions\ViewService;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\SearchService;
+use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
class SearchController extends Controller
return view('search/entity-ajax-list', ['entities' => $entities]);
}
+
+ /**
+ * Search siblings items in the system.
+ * @param Request $request
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|mixed
+ */
+ public function searchSiblings(Request $request)
+ {
+ $type = $request->get('entity_type', null);
+ $id = $request->get('entity_id', null);
+
+ $entity = $this->entityRepo->getById($type, $id);
+ if (!$entity) {
+ return $this->jsonError(trans('errors.entity_not_found'), 404);
+ }
+
+ $entities = [];
+
+ // Page in chapter
+ if ($entity->isA('page') && $entity->chapter) {
+ $entities = $this->entityRepo->getChapterChildren($entity->chapter);
+ }
+
+ // Page in book or chapter
+ if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
+ $entities = $this->entityRepo->getBookDirectChildren($entity->book);
+ }
+
+ // Book in shelf
+ // TODO - When shelve tracking added, Update below if criteria
+
+ // Book
+ if ($entity->isA('book')) {
+ $entities = $this->entityRepo->getAll('book');
+ }
+
+ // Shelve
+ // TODO - When shelve tracking added
+
+ return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
+ }
}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ viewBox="0 0 24 24"
+ version="1.1"
+ id="svg6"
+ sodipodi:docname="books.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
+ <metadata
+ id="metadata12">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs10" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="2560"
+ inkscape:window-height="1413"
+ id="namedview8"
+ showgrid="false"
+ inkscape:zoom="19.666667"
+ inkscape:cx="13.076733"
+ inkscape:cy="8.7801453"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg6" />
+ <path
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="M 19.252119,1.707627 H 8.6631356 c -0.9706568,0 -1.7648305,0.7941737 -1.7648305,1.7648305 V 17.591101 c 0,0.970657 0.7941737,1.764831 1.7648305,1.764831 H 19.252119 c 0.970656,0 1.76483,-0.794174 1.76483,-1.764831 V 3.4724575 c 0,-0.9706568 -0.794174,-1.7648305 -1.76483,-1.7648305 z M 8.6631356,3.4724575 H 13.075212 V 10.531779 L 10.869173,9.2081571 8.6631356,10.531779 Z"
+ id="path4"
+ inkscape:connector-curvature="0"
+ style="stroke-width:0.88241524" />
+ <g
+ id="g836"
+ transform="translate(30.610169,3.2033898)">
+ <path
+ id="path822"
+ d="M 0,0 H 24 V 24 H 0 Z"
+ inkscape:connector-curvature="0"
+ style="fill:none" />
+ <path
+ id="path824"
+ d="M -27.644068,3.4067797 V 17.40678 c 0,1.1 0.9,2 2,2 h 14 v -2 h -14 V 3.4067797 Z"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cssccccc" />
+ </g>
+</svg>
--- /dev/null
+
+
+class BreadcrumbListing {
+
+ constructor(elem) {
+ this.elem = elem;
+ this.searchInput = elem.querySelector('input');
+ this.loadingElem = elem.querySelector('.loading-container');
+ this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
+ this.toggleElem = elem.querySelector('[dropdown-toggle]');
+
+ // this.loadingElem.style.display = 'none';
+ const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
+ this.entityType = entityDescriptor[0];
+ this.entityId = Number(entityDescriptor[1]);
+
+ this.toggleElem.addEventListener('click', this.onShow.bind(this));
+ this.searchInput.addEventListener('input', this.onSearch.bind(this));
+ }
+
+ onShow() {
+ this.loadEntityView();
+ }
+
+ onSearch() {
+ const input = this.searchInput.value.toLowerCase().trim();
+ const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
+ console.log(listItems);
+ for (let listItem of listItems) {
+ const match = !input || listItem.textContent.toLowerCase().includes(input);
+ console.log(match);
+ listItem.style.display = match ? 'flex' : 'none';
+ }
+ }
+
+ loadEntityView() {
+ this.toggleLoading(true);
+
+ const params = {
+ 'entity_id': this.entityId,
+ 'entity_type': this.entityType,
+ };
+
+ window.$http.get('/search/entity/siblings', {params}).then(resp => {
+ this.entityListElem.innerHTML = resp.data;
+ }).catch(err => {
+ console.error(err);
+ }).then(() => {
+ this.toggleLoading(false);
+ this.onSearch();
+ });
+ }
+
+ toggleLoading(show = false) {
+ this.loadingElem.style.display = show ? 'block' : 'none';
+ }
+
+}
+
+export default BreadcrumbListing;
\ No newline at end of file
constructor(elem) {
this.container = elem;
- this.menu = elem.querySelector('ul');
+ this.menu = elem.querySelector('ul, [dropdown-menu]');
this.toggle = elem.querySelector('[dropdown-toggle]');
this.setupListeners();
}
import headerMobileToggle from "./header-mobile-toggle";
import listSortControl from "./list-sort-control";
import triLayout from "./tri-layout";
-
+import breadcrumbListing from "./breadcrumb-listing";
const componentMapping = {
'dropdown': dropdown,
'header-mobile-toggle': headerMobileToggle,
'list-sort-control': listSortControl,
'tri-layout': triLayout,
+ 'breadcrumb-listing': breadcrumbListing,
};
window.components = {};
}
}
+.breadcrumb-listing {
+ position: relative;
+ .breadcrumb-listing-toggle {
+ padding: 6px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ &:hover {
+ border-color: #DDD;
+ }
+ }
+ .svg-icon {
+ margin-right: 0;
+ }
+}
+
+.breadcrumb-listing-dropdown {
+ box-shadow: $bs-med;
+ overflow: hidden;
+ min-height: 100px;
+ width: 240px;
+ display: none;
+ position: absolute;
+ z-index: 80;
+ right: -$-m;
+ .breadcrumb-listing-search .svg-icon {
+ position: absolute;
+ left: $-s;
+ top: 11px;
+ fill: #888;
+ pointer-events: none;
+ }
+ .breadcrumb-listing-entity-list {
+ max-height: 400px;
+ overflow-y: scroll;
+ text-align: left;
+ }
+ input {
+ padding-left: $-xl;
+ border-radius: 0;
+ border: 0;
+ border-bottom: 1px solid #DDD;
+ }
+}
+
.faded {
a, button, span, span > div {
color: #666;
/**
* Icons
*/
-i {
- padding-right: $-xs;
-}
-
.svg-icon {
width: 1em;
height: 1em;
position: relative;
bottom: -0.105em;
margin-right: $-xs;
+ pointer-events: none;
}
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
+ @include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
@if($currentUser->can('book-create-all'))
<a href="{{ baseUrl("/create-book") }}" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.books_create') }}</span>
</a>
@endif
- @include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
</div>
</div>
@section('body')
+ <div class="mb-s">
+ @include('partials.breadcrumbs', ['crumbs' => [
+ $book,
+ ]])
+ </div>
+
<div class="content-wrap card">
<h1 class="break-text" v-pre>{{$book->name}}</h1>
<div class="book-content" v-show="!searching">
@if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
<a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
- <a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
+ <a href="{{ baseUrl('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
@if(signedInUser() && userCan('settings-manage'))
<a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
@endif
--- /dev/null
+<div class="breadcrumb-listing" dropdown breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
+ <div class="breadcrumb-listing-toggle" dropdown-toggle>
+ <div class="separator">@icon('chevron-right')</div>
+ </div>
+ <div dropdown-menu class="breadcrumb-listing-dropdown card">
+ <div class="breadcrumb-listing-search">
+ @icon('search')
+ <input autocomplete="off" type="text" name="entity-search">
+ </div>
+ @include('partials.loading-icon')
+ <div class="breadcrumb-listing-entity-list px-m"></div>
+ </div>
+</div>
\ No newline at end of file
<div class="breadcrumbs text-center">
<?php $breadcrumbCount = 0; ?>
+
+ {{--Show top level item--}}
+ @if (count($crumbs) > 0 && $crumbs[0] instanceof \BookStack\Entities\Book)
+ <a href="{{ baseUrl('/books') }}" class="icon-list-item">
+ <span>@icon('books')</span>
+ <span>{{ trans('entities.books') }}</span>
+ </a>
+ <?php $breadcrumbCount++; ?>
+ @endif
+
@foreach($crumbs as $key => $crumb)
+ <?php $isEntity = ($crumb instanceof \BookStack\Entities\Entity); ?>
+
@if (is_null($crumb))
<?php continue; ?>
@endif
- @if ($breadcrumbCount !== 0)
+ @if ($breadcrumbCount !== 0 && !$isEntity)
<div class="separator">@icon('chevron-right')</div>
@endif
<span>@icon($crumb['icon'])</span>
<span>{{ $crumb['text'] }}</span>
</a>
- @elseif($crumb instanceof \BookStack\Entities\Entity)
+ @elseif($isEntity && userCan('view', $crumb))
+ @if($breadcrumbCount > 0)
+ @include('partials.breadcrumb-listing', ['entity' => $crumb])
+ @endif
<a href="{{ $crumb->getUrl() }}" class="text-{{$crumb->getType()}} icon-list-item">
<span>@icon($crumb->getType())</span>
- <span>{{ $crumb->getShortName() }}</span>
+ <span>
+ {{ $crumb->getShortName() }}
+ </span>
</a>
@endif
<?php $breadcrumbCount++; ?>
--- /dev/null
+<div class="entity-list {{ $style ?? '' }}">
+ @if(count($entities) > 0)
+ @foreach($entities as $index => $entity)
+ @include('partials.entity-list-item-basic', ['entity' => $entity])
+ @endforeach
+ @else
+ <p class="text-muted empty-text">
+ {{ $emptyText ?? trans('common.no_items') }}
+ </p>
+ @endif
+</div>
\ No newline at end of file
{{--<div class="toolbar px-xl">--}}
{{--@yield('toolbar')--}}
{{--</div>--}}
+ {{--TODO - Cleanup toolbar usage--}}
<div class="tri-layout-container mt-m" tri-layout @yield('container-attrs') >
Route::get('/search', 'SearchController@search');
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
+ Route::get('/search/entity/siblings', 'SearchController@searchSiblings');
// Other Pages
Route::get('/', 'HomeController@index');