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]);
$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()]));
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 13.114h-4.886V18h-2.228v-4.886H6v-2.228h4.886V6h2.228v4.886H18Z" style="stroke-width:.857143"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.5 7.91 16.09 6.5 12 10.59 7.91 6.5 6.5 7.91 10.59 12 6.5 16.09l1.41 1.41L12 13.41l4.09 4.09 1.41-1.41L13.41 12Z"/></svg>
\ No newline at end of file
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() {
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();
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);
+ }
+ });
}
/**
}
/**
- * 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();
}
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();
+ }
+
}
\ No newline at end of file
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;
}
\ No newline at end of file
}
}
-.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;
<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>
--- /dev/null
+<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>
\ No newline at end of file