mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-03-09 03:35:51 +08:00
Added experimental breadcrumb traversal
This commit is contained in:
parent
e70423c73f
commit
035a0d8efb
@ -69,6 +69,15 @@ class Book extends Entity
|
||||
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
|
||||
|
@ -341,6 +341,18 @@ class EntityRepo
|
||||
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.
|
||||
|
@ -3,6 +3,7 @@
|
||||
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
|
||||
@ -104,4 +105,45 @@ 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']);
|
||||
}
|
||||
}
|
||||
|
71
resources/assets/icons/books.svg
Normal file
71
resources/assets/icons/books.svg
Normal file
@ -0,0 +1,71 @@
|
||||
<?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>
|
After Width: | Height: | Size: 2.3 KiB |
60
resources/assets/js/components/breadcrumb-listing.js
Normal file
60
resources/assets/js/components/breadcrumb-listing.js
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
|
||||
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;
|
@ -6,7 +6,7 @@ class DropDown {
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import homepageControl from "./homepage-control";
|
||||
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,
|
||||
@ -47,6 +47,7 @@ const componentMapping = {
|
||||
'header-mobile-toggle': headerMobileToggle,
|
||||
'list-sort-control': listSortControl,
|
||||
'tri-layout': triLayout,
|
||||
'breadcrumb-listing': breadcrumbListing,
|
||||
};
|
||||
|
||||
window.components = {};
|
||||
|
@ -220,6 +220,50 @@ header .search-box {
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -340,10 +340,6 @@ span.sep {
|
||||
/**
|
||||
* Icons
|
||||
*/
|
||||
i {
|
||||
padding-right: $-xs;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
@ -351,5 +347,6 @@ i {
|
||||
position: relative;
|
||||
bottom: -0.105em;
|
||||
margin-right: $-xs;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -38,13 +38,13 @@
|
||||
<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>
|
||||
|
||||
|
@ -8,6 +8,12 @@
|
||||
|
||||
@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">
|
||||
|
@ -30,7 +30,7 @@
|
||||
@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
|
||||
|
13
resources/views/partials/breadcrumb-listing.blade.php
Normal file
13
resources/views/partials/breadcrumb-listing.blade.php
Normal file
@ -0,0 +1,13 @@
|
||||
<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>
|
@ -1,10 +1,22 @@
|
||||
<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
|
||||
|
||||
@ -17,10 +29,15 @@
|
||||
<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++; ?>
|
||||
|
11
resources/views/partials/entity-list-basic.blade.php
Normal file
11
resources/views/partials/entity-list-basic.blade.php
Normal file
@ -0,0 +1,11 @@
|
||||
<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>
|
@ -7,6 +7,7 @@
|
||||
{{--<div class="toolbar px-xl">--}}
|
||||
{{--@yield('toolbar')--}}
|
||||
{{--</div>--}}
|
||||
{{--TODO - Cleanup toolbar usage--}}
|
||||
|
||||
<div class="tri-layout-container mt-m" tri-layout @yield('container-attrs') >
|
||||
|
||||
|
@ -154,6 +154,7 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user