Skip to content

Shelf book sort improvements #4049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/Http/Controllers/BookshelfController.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function index(Request $request)
public function create()
{
$this->checkPermission('bookshelf-create-all');
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug']);
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_create'));

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

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

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

Expand Down
1 change: 1 addition & 0 deletions resources/icons/add-small.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/icons/remove.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 69 additions & 12 deletions resources/js/components/shelf-sort.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import Sortable from "sortablejs";
import {Component} from "./component";

/**
* @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
*/
const itemActions = {
move_up(item, shelfBooksList, allBooksList) {
const list = item.parentNode;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.max(index - 1, 0);
list.insertBefore(item, list.children[newIndex] || null);
},
move_down(item, shelfBooksList, allBooksList) {
const list = item.parentNode;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.min(index + 2, list.children.length);
list.insertBefore(item, list.children[newIndex] || null);
},
remove(item, shelfBooksList, allBooksList) {
allBooksList.appendChild(item);
},
add(item, shelfBooksList, allBooksList) {
shelfBooksList.appendChild(item);
},
};

export class ShelfSort extends Component {

setup() {
Expand All @@ -9,6 +33,9 @@ export class ShelfSort extends Component {
this.shelfBookList = this.$refs.shelfBookList;
this.allBookList = this.$refs.allBookList;
this.bookSearchInput = this.$refs.bookSearch;
this.sortButtonContainer = this.$refs.sortButtonContainer;

this.lastSort = null;

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

setupListeners() {
this.elem.addEventListener('click', event => {
const sortItem = event.target.closest('.scroll-box-item');
if (sortItem) {
event.preventDefault();
this.sortItemClick(sortItem);
const sortItemAction = event.target.closest('.scroll-box-item button[data-action]');
if (sortItemAction) {
this.sortItemActionClick(sortItemAction);
}
});

this.bookSearchInput.addEventListener('input', event => {
this.filterBooksByName(this.bookSearchInput.value);
});

this.sortButtonContainer.addEventListener('click' , event => {
const button = event.target.closest('button[data-sort]');
if (button) {
this.sortShelfBooks(button.dataset.sort);
}
});
}

/**
Expand All @@ -62,15 +95,16 @@ export class ShelfSort extends Component {
}

/**
* Called when a sort item is clicked.
* @param {Element} sortItem
* Called when a sort item action button is clicked.
* @param {HTMLElement} sortItemAction
*/
sortItemClick(sortItem) {
const lists = this.elem.querySelectorAll('.scroll-box');
const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
if (newList.length > 0) {
newList[0].appendChild(sortItem);
}
sortItemActionClick(sortItemAction) {
const sortItem = sortItemAction.closest('.scroll-box-item');
const action = sortItemAction.dataset.action;

const actionFunction = itemActions[action];
actionFunction(sortItem, this.shelfBookList, this.allBookList);

this.onChange();
}

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

sortShelfBooks(sortProperty) {
const books = Array.from(this.shelfBookList.children);
const reverse = sortProperty === this.lastSort;

books.sort((bookA, bookB) => {
const aProp = bookA.dataset[sortProperty].toLowerCase();
const bProp = bookB.dataset[sortProperty].toLowerCase();

if (reverse) {
return aProp < bProp ? (aProp === bProp ? 0 : 1) : -1;
}

return aProp < bProp ? (aProp === bProp ? 0 : -1) : 1;
});

for (const book of books) {
this.shelfBookList.append(book);
}

this.lastSort = (this.lastSort === sortProperty) ? null : sortProperty;
this.onChange();
}

}
85 changes: 85 additions & 0 deletions resources/sass/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1050,4 +1050,89 @@ $btt-size: 40px;
vertical-align: top;
line-height: 2;
}
}

// Sortable scroll boxes
.scroll-box {
list-style: none;
padding: 0;
margin: 0;
max-height: 280px;
overflow-y: scroll;
border: 1px solid;
@include lightDark(border-color, #DDD, #000);
border-radius: 3px;
min-height: 20px;
@include lightDark(background-color, #EEE, #000);
}
.scroll-box-item {
border-bottom: 1px solid;
border-top: 1px solid;
@include lightDark(border-color, #DDD, #000);
margin-top: -1px;
@include lightDark(background-color, #FFF, #222);
display: flex;
align-items: flex-start;
padding: 1px;
&:last-child {
border-bottom: 0;
}
&:hover {
cursor: pointer;
@include lightDark(background-color, #f8f8f8, #333);
}
.handle {
color: #AAA;
cursor: grab;
}
button {
opacity: .6;
}
.handle svg {
margin: 0;
}
> * {
padding: $-xs $-m;
}
.handle + * {
padding-left: 0;
}
&:hover .handle {
@include lightDark(color, #444, #FFF);
}
&:hover button {
opacity: 1;
}
a:hover {
text-decoration: none;
}
}

input.scroll-box-search, .scroll-box-header-item {
font-size: 0.8rem;
border: 1px solid;
@include lightDark(border-color, #DDD, #000);
@include lightDark(background-color, #FFF, #222);
margin-bottom: -1px;
border-radius: 3px 3px 0 0;
width: 100%;
max-width: 100%;
height: auto;
line-height: 1.4;
color: #666;
}

.scroll-box-search + .scroll-box,
.scroll-box-header-item + .scroll-box {
border-radius: 0 0 3px 3px;
}

.scroll-box[refs="shelf-sort@shelf-book-list"] [data-action="add"] {
display: none;
}
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="remove"],
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_up"],
.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_down"],
{
display: none;
}
65 changes: 0 additions & 65 deletions resources/sass/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -198,71 +198,6 @@ $loadingSize: 10px;
}
}

.scroll-box {
max-height: 250px;
overflow-y: scroll;
border: 1px solid;
@include lightDark(border-color, #DDD, #000);
border-radius: 3px;
min-height: 20px;
@include lightDark(background-color, #EEE, #000);
}
.scroll-box-item {
border-bottom: 1px solid;
border-top: 1px solid;
@include lightDark(border-color, #DDD, #000);
margin-top: -1px;
@include lightDark(background-color, #FFF, #222);
display: flex;
padding: 1px;
&:last-child {
border-bottom: 0;
}
&:hover {
cursor: pointer;
@include lightDark(background-color, #f8f8f8, #333);
}
.handle {
color: #AAA;
cursor: grab;
}
.handle svg {
margin: 0;
}
> * {
padding: $-xs $-m;
}
.handle + * {
padding-left: 0;
}
&:hover .handle {
@include lightDark(color, #444, #FFF);
}
a:hover {
text-decoration: none;
}
}

input.scroll-box-search, .scroll-box-header-item {
font-size: 0.8rem;
padding: $-xs $-m;
border: 1px solid;
@include lightDark(border-color, #DDD, #000);
@include lightDark(background-color, #FFF, #222);
margin-bottom: -1px;
border-radius: 3px 3px 0 0;
width: 100%;
max-width: 100%;
height: auto;
line-height: 1.4;
color: #666;
}

.scroll-box-search + .scroll-box,
.scroll-box-header-item + .scroll-box {
border-radius: 0 0 3px 3px;
}

.fullscreen {
border:0;
position:fixed;
Expand Down
49 changes: 31 additions & 18 deletions resources/views/shelves/parts/form.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,45 @@

<div component="shelf-sort" class="grid half gap-xl">
<div class="form-group">
<label for="books">{{ trans('entities.shelves_books') }}</label>
<label for="books" id="shelf-sort-books-label">{{ trans('entities.shelves_books') }}</label>
<input refs="shelf-sort@input" type="hidden" name="books"
value="{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}">
<div class="scroll-box-header-item">{{ trans('entities.shelves_drag_books') }}</div>
<div refs="shelf-sort@shelf-book-list" class="scroll-box">
@if (count($shelf->visibleBooks ?? []) > 0)
@foreach ($shelf->visibleBooks as $book)
<div data-id="{{ $book->id }}" class="scroll-box-item">
<div class="handle">@icon('grip')</div>
<a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
</div>
@endforeach
@endif
<div class="scroll-box-header-item flex-container-row items-center py-xs">
<span class="px-m py-xs">{{ trans('entities.shelves_drag_books') }}</span>
<div class="dropdown-container ml-auto" component="dropdown">
<button refs="dropdown@toggle"
type="button"
title="{{ trans('common.more') }}"
class="icon-button px-xs py-xxs mx-xs text-bigger"
aria-haspopup="true"
aria-expanded="false">
@icon('more')
</button>
<div refs="dropdown@menu shelf-sort@sort-button-container" class="dropdown-menu" role="menu">
<button type="button" class="text-item" data-sort="name">{{ trans('entities.books_sort_name') }}</button>
<button type="button" class="text-item" data-sort="created">{{ trans('entities.books_sort_created') }}</button>
<button type="button" class="text-item" data-sort="updated">{{ trans('entities.books_sort_updated') }}</button>
</div>
</div>
</div>
<ul refs="shelf-sort@shelf-book-list"
aria-labelledby="shelf-sort-books-label"
class="scroll-box">
@foreach (($shelf->visibleBooks ?? []) as $book)
@include('shelves.parts.shelf-sort-book-item', ['book' => $book])
@endforeach
</ul>
</div>
<div class="form-group">
<label for="books">{{ trans('entities.shelves_add_books') }}</label>
<label for="books" id="shelf-sort-all-books-label">{{ trans('entities.shelves_add_books') }}</label>
<input type="text" refs="shelf-sort@book-search" class="scroll-box-search" placeholder="{{ trans('common.search') }}">
<div refs="shelf-sort@all-book-list" class="scroll-box">
<ul refs="shelf-sort@all-book-list"
aria-labelledby="shelf-sort-all-books-label"
class="scroll-box">
@foreach ($books as $book)
<div data-id="{{ $book->id }}" class="scroll-box-item">
<div class="handle">@icon('grip')</div>
<a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
</div>
@include('shelves.parts.shelf-sort-book-item', ['book' => $book])
@endforeach
</div>
</ul>
</div>
</div>

Expand Down
18 changes: 18 additions & 0 deletions resources/views/shelves/parts/shelf-sort-book-item.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<li data-id="{{ $book->id }}"
data-name="{{ $book->name }}"
data-created="{{ $book->created_at->timestamp }}"
data-updated="{{ $book->updated_at->timestamp }}"
class="scroll-box-item">
<div class="handle px-s">@icon('grip')</div>
<div class="text-book">@icon('book'){{ $book->name }}</div>
<div class="buttons flex-container-row items-center ml-auto px-xxs py-xs">
<button type="button" data-action="move_up" class="icon-button p-xxs"
title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
<button type="button" data-action="move_down" class="icon-button p-xxs"
title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
<button type="button" data-action="remove" class="icon-button p-xxs"
title="{{ trans('common.remove') }}">@icon('remove')</button>
<button type="button" data-action="add" class="icon-button p-xxs"
title="{{ trans('common.add') }}">@icon('add-small')</button>
</div>
</li>