Skip to content

Commit 524adce

Browse files
authored
Merge pull request #4049 from BookStackApp/shelf_book_sort_updates
Shelf book sort improvements
2 parents af31a6f + f799c9b commit 524adce

File tree

8 files changed

+207
-97
lines changed

8 files changed

+207
-97
lines changed

app/Http/Controllers/BookshelfController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function index(Request $request)
6464
public function create()
6565
{
6666
$this->checkPermission('bookshelf-create-all');
67-
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug']);
67+
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
6868
$this->setPageTitle(trans('entities.shelves_create'));
6969

7070
return view('shelves.create', ['books' => $books]);
@@ -140,7 +140,7 @@ public function edit(string $slug)
140140
$this->checkOwnablePermission('bookshelf-update', $shelf);
141141

142142
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
143-
$books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug']);
143+
$books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
144144

145145
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
146146

resources/icons/add-small.svg

Lines changed: 1 addition & 0 deletions
Loading

resources/icons/remove.svg

Lines changed: 1 addition & 0 deletions
Loading

resources/js/components/shelf-sort.js

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
import Sortable from "sortablejs";
22
import {Component} from "./component";
33

4+
/**
5+
* @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
6+
*/
7+
const itemActions = {
8+
move_up(item, shelfBooksList, allBooksList) {
9+
const list = item.parentNode;
10+
const index = Array.from(list.children).indexOf(item);
11+
const newIndex = Math.max(index - 1, 0);
12+
list.insertBefore(item, list.children[newIndex] || null);
13+
},
14+
move_down(item, shelfBooksList, allBooksList) {
15+
const list = item.parentNode;
16+
const index = Array.from(list.children).indexOf(item);
17+
const newIndex = Math.min(index + 2, list.children.length);
18+
list.insertBefore(item, list.children[newIndex] || null);
19+
},
20+
remove(item, shelfBooksList, allBooksList) {
21+
allBooksList.appendChild(item);
22+
},
23+
add(item, shelfBooksList, allBooksList) {
24+
shelfBooksList.appendChild(item);
25+
},
26+
};
27+
428
export class ShelfSort extends Component {
529

630
setup() {
@@ -9,6 +33,9 @@ export class ShelfSort extends Component {
933
this.shelfBookList = this.$refs.shelfBookList;
1034
this.allBookList = this.$refs.allBookList;
1135
this.bookSearchInput = this.$refs.bookSearch;
36+
this.sortButtonContainer = this.$refs.sortButtonContainer;
37+
38+
this.lastSort = null;
1239

1340
this.initSortable();
1441
this.setupListeners();
@@ -29,16 +56,22 @@ export class ShelfSort extends Component {
2956

3057
setupListeners() {
3158
this.elem.addEventListener('click', event => {
32-
const sortItem = event.target.closest('.scroll-box-item');
33-
if (sortItem) {
34-
event.preventDefault();
35-
this.sortItemClick(sortItem);
59+
const sortItemAction = event.target.closest('.scroll-box-item button[data-action]');
60+
if (sortItemAction) {
61+
this.sortItemActionClick(sortItemAction);
3662
}
3763
});
3864

3965
this.bookSearchInput.addEventListener('input', event => {
4066
this.filterBooksByName(this.bookSearchInput.value);
4167
});
68+
69+
this.sortButtonContainer.addEventListener('click' , event => {
70+
const button = event.target.closest('button[data-sort]');
71+
if (button) {
72+
this.sortShelfBooks(button.dataset.sort);
73+
}
74+
});
4275
}
4376

4477
/**
@@ -62,15 +95,16 @@ export class ShelfSort extends Component {
6295
}
6396

6497
/**
65-
* Called when a sort item is clicked.
66-
* @param {Element} sortItem
98+
* Called when a sort item action button is clicked.
99+
* @param {HTMLElement} sortItemAction
67100
*/
68-
sortItemClick(sortItem) {
69-
const lists = this.elem.querySelectorAll('.scroll-box');
70-
const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
71-
if (newList.length > 0) {
72-
newList[0].appendChild(sortItem);
73-
}
101+
sortItemActionClick(sortItemAction) {
102+
const sortItem = sortItemAction.closest('.scroll-box-item');
103+
const action = sortItemAction.dataset.action;
104+
105+
const actionFunction = itemActions[action];
106+
actionFunction(sortItem, this.shelfBookList, this.allBookList);
107+
74108
this.onChange();
75109
}
76110

@@ -79,4 +113,27 @@ export class ShelfSort extends Component {
79113
this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
80114
}
81115

116+
sortShelfBooks(sortProperty) {
117+
const books = Array.from(this.shelfBookList.children);
118+
const reverse = sortProperty === this.lastSort;
119+
120+
books.sort((bookA, bookB) => {
121+
const aProp = bookA.dataset[sortProperty].toLowerCase();
122+
const bProp = bookB.dataset[sortProperty].toLowerCase();
123+
124+
if (reverse) {
125+
return aProp < bProp ? (aProp === bProp ? 0 : 1) : -1;
126+
}
127+
128+
return aProp < bProp ? (aProp === bProp ? 0 : -1) : 1;
129+
});
130+
131+
for (const book of books) {
132+
this.shelfBookList.append(book);
133+
}
134+
135+
this.lastSort = (this.lastSort === sortProperty) ? null : sortProperty;
136+
this.onChange();
137+
}
138+
82139
}

resources/sass/_components.scss

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,4 +1050,89 @@ $btt-size: 40px;
10501050
vertical-align: top;
10511051
line-height: 2;
10521052
}
1053+
}
1054+
1055+
// Sortable scroll boxes
1056+
.scroll-box {
1057+
list-style: none;
1058+
padding: 0;
1059+
margin: 0;
1060+
max-height: 280px;
1061+
overflow-y: scroll;
1062+
border: 1px solid;
1063+
@include lightDark(border-color, #DDD, #000);
1064+
border-radius: 3px;
1065+
min-height: 20px;
1066+
@include lightDark(background-color, #EEE, #000);
1067+
}
1068+
.scroll-box-item {
1069+
border-bottom: 1px solid;
1070+
border-top: 1px solid;
1071+
@include lightDark(border-color, #DDD, #000);
1072+
margin-top: -1px;
1073+
@include lightDark(background-color, #FFF, #222);
1074+
display: flex;
1075+
align-items: flex-start;
1076+
padding: 1px;
1077+
&:last-child {
1078+
border-bottom: 0;
1079+
}
1080+
&:hover {
1081+
cursor: pointer;
1082+
@include lightDark(background-color, #f8f8f8, #333);
1083+
}
1084+
.handle {
1085+
color: #AAA;
1086+
cursor: grab;
1087+
}
1088+
button {
1089+
opacity: .6;
1090+
}
1091+
.handle svg {
1092+
margin: 0;
1093+
}
1094+
> * {
1095+
padding: $-xs $-m;
1096+
}
1097+
.handle + * {
1098+
padding-left: 0;
1099+
}
1100+
&:hover .handle {
1101+
@include lightDark(color, #444, #FFF);
1102+
}
1103+
&:hover button {
1104+
opacity: 1;
1105+
}
1106+
a:hover {
1107+
text-decoration: none;
1108+
}
1109+
}
1110+
1111+
input.scroll-box-search, .scroll-box-header-item {
1112+
font-size: 0.8rem;
1113+
border: 1px solid;
1114+
@include lightDark(border-color, #DDD, #000);
1115+
@include lightDark(background-color, #FFF, #222);
1116+
margin-bottom: -1px;
1117+
border-radius: 3px 3px 0 0;
1118+
width: 100%;
1119+
max-width: 100%;
1120+
height: auto;
1121+
line-height: 1.4;
1122+
color: #666;
1123+
}
1124+
1125+
.scroll-box-search + .scroll-box,
1126+
.scroll-box-header-item + .scroll-box {
1127+
border-radius: 0 0 3px 3px;
1128+
}
1129+
1130+
.scroll-box[refs="shelf-sort@shelf-book-list"] [data-action="add"] {
1131+
display: none;
1132+
}
1133+
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="remove"],
1134+
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_up"],
1135+
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_down"],
1136+
{
1137+
display: none;
10531138
}

resources/sass/styles.scss

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -198,71 +198,6 @@ $loadingSize: 10px;
198198
}
199199
}
200200

201-
.scroll-box {
202-
max-height: 250px;
203-
overflow-y: scroll;
204-
border: 1px solid;
205-
@include lightDark(border-color, #DDD, #000);
206-
border-radius: 3px;
207-
min-height: 20px;
208-
@include lightDark(background-color, #EEE, #000);
209-
}
210-
.scroll-box-item {
211-
border-bottom: 1px solid;
212-
border-top: 1px solid;
213-
@include lightDark(border-color, #DDD, #000);
214-
margin-top: -1px;
215-
@include lightDark(background-color, #FFF, #222);
216-
display: flex;
217-
padding: 1px;
218-
&:last-child {
219-
border-bottom: 0;
220-
}
221-
&:hover {
222-
cursor: pointer;
223-
@include lightDark(background-color, #f8f8f8, #333);
224-
}
225-
.handle {
226-
color: #AAA;
227-
cursor: grab;
228-
}
229-
.handle svg {
230-
margin: 0;
231-
}
232-
> * {
233-
padding: $-xs $-m;
234-
}
235-
.handle + * {
236-
padding-left: 0;
237-
}
238-
&:hover .handle {
239-
@include lightDark(color, #444, #FFF);
240-
}
241-
a:hover {
242-
text-decoration: none;
243-
}
244-
}
245-
246-
input.scroll-box-search, .scroll-box-header-item {
247-
font-size: 0.8rem;
248-
padding: $-xs $-m;
249-
border: 1px solid;
250-
@include lightDark(border-color, #DDD, #000);
251-
@include lightDark(background-color, #FFF, #222);
252-
margin-bottom: -1px;
253-
border-radius: 3px 3px 0 0;
254-
width: 100%;
255-
max-width: 100%;
256-
height: auto;
257-
line-height: 1.4;
258-
color: #666;
259-
}
260-
261-
.scroll-box-search + .scroll-box,
262-
.scroll-box-header-item + .scroll-box {
263-
border-radius: 0 0 3px 3px;
264-
}
265-
266201
.fullscreen {
267202
border:0;
268203
position:fixed;

resources/views/shelves/parts/form.blade.php

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,45 @@
1212

1313
<div component="shelf-sort" class="grid half gap-xl">
1414
<div class="form-group">
15-
<label for="books">{{ trans('entities.shelves_books') }}</label>
15+
<label for="books" id="shelf-sort-books-label">{{ trans('entities.shelves_books') }}</label>
1616
<input refs="shelf-sort@input" type="hidden" name="books"
1717
value="{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}">
18-
<div class="scroll-box-header-item">{{ trans('entities.shelves_drag_books') }}</div>
19-
<div refs="shelf-sort@shelf-book-list" class="scroll-box">
20-
@if (count($shelf->visibleBooks ?? []) > 0)
21-
@foreach ($shelf->visibleBooks as $book)
22-
<div data-id="{{ $book->id }}" class="scroll-box-item">
23-
<div class="handle">@icon('grip')</div>
24-
<a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
25-
</div>
26-
@endforeach
27-
@endif
18+
<div class="scroll-box-header-item flex-container-row items-center py-xs">
19+
<span class="px-m py-xs">{{ trans('entities.shelves_drag_books') }}</span>
20+
<div class="dropdown-container ml-auto" component="dropdown">
21+
<button refs="dropdown@toggle"
22+
type="button"
23+
title="{{ trans('common.more') }}"
24+
class="icon-button px-xs py-xxs mx-xs text-bigger"
25+
aria-haspopup="true"
26+
aria-expanded="false">
27+
@icon('more')
28+
</button>
29+
<div refs="dropdown@menu shelf-sort@sort-button-container" class="dropdown-menu" role="menu">
30+
<button type="button" class="text-item" data-sort="name">{{ trans('entities.books_sort_name') }}</button>
31+
<button type="button" class="text-item" data-sort="created">{{ trans('entities.books_sort_created') }}</button>
32+
<button type="button" class="text-item" data-sort="updated">{{ trans('entities.books_sort_updated') }}</button>
33+
</div>
34+
</div>
2835
</div>
36+
<ul refs="shelf-sort@shelf-book-list"
37+
aria-labelledby="shelf-sort-books-label"
38+
class="scroll-box">
39+
@foreach (($shelf->visibleBooks ?? []) as $book)
40+
@include('shelves.parts.shelf-sort-book-item', ['book' => $book])
41+
@endforeach
42+
</ul>
2943
</div>
3044
<div class="form-group">
31-
<label for="books">{{ trans('entities.shelves_add_books') }}</label>
45+
<label for="books" id="shelf-sort-all-books-label">{{ trans('entities.shelves_add_books') }}</label>
3246
<input type="text" refs="shelf-sort@book-search" class="scroll-box-search" placeholder="{{ trans('common.search') }}">
33-
<div refs="shelf-sort@all-book-list" class="scroll-box">
47+
<ul refs="shelf-sort@all-book-list"
48+
aria-labelledby="shelf-sort-all-books-label"
49+
class="scroll-box">
3450
@foreach ($books as $book)
35-
<div data-id="{{ $book->id }}" class="scroll-box-item">
36-
<div class="handle">@icon('grip')</div>
37-
<a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
38-
</div>
51+
@include('shelves.parts.shelf-sort-book-item', ['book' => $book])
3952
@endforeach
40-
</div>
53+
</ul>
4154
</div>
4255
</div>
4356

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<li data-id="{{ $book->id }}"
2+
data-name="{{ $book->name }}"
3+
data-created="{{ $book->created_at->timestamp }}"
4+
data-updated="{{ $book->updated_at->timestamp }}"
5+
class="scroll-box-item">
6+
<div class="handle px-s">@icon('grip')</div>
7+
<div class="text-book">@icon('book'){{ $book->name }}</div>
8+
<div class="buttons flex-container-row items-center ml-auto px-xxs py-xs">
9+
<button type="button" data-action="move_up" class="icon-button p-xxs"
10+
title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
11+
<button type="button" data-action="move_down" class="icon-button p-xxs"
12+
title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
13+
<button type="button" data-action="remove" class="icon-button p-xxs"
14+
title="{{ trans('common.remove') }}">@icon('remove')</button>
15+
<button type="button" data-action="add" class="icon-button p-xxs"
16+
title="{{ trans('common.add') }}">@icon('add-small')</button>
17+
</div>
18+
</li>

0 commit comments

Comments
 (0)