mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-15 17:24:11 +00:00
Merge pull request #3999 from BookStackApp/sort_ui_improvements
Improve Book Sorting User Experience
This commit is contained in:
commit
d7723b33f3
11 changed files with 314 additions and 59 deletions
resources
js
lang/en
sass
views
|
@ -1,4 +1,4 @@
|
||||||
import Sortable from "sortablejs";
|
import Sortable, {MultiDrag} from "sortablejs";
|
||||||
import {Component} from "./component";
|
import {Component} from "./component";
|
||||||
import {htmlToDom} from "../services/dom";
|
import {htmlToDom} from "../services/dom";
|
||||||
|
|
||||||
|
@ -37,6 +37,113 @@ const sortOperations = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available move actions.
|
||||||
|
* The active function indicates if the action is possible for the given item.
|
||||||
|
* The run function performs the move.
|
||||||
|
* @type {{up: {active(Element, ?Element, Element): boolean, run(Element, ?Element, Element)}}}
|
||||||
|
*/
|
||||||
|
const moveActions = {
|
||||||
|
up: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return !(elem.previousElementSibling === null && !parent);
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
const newSibling = elem.previousElementSibling || parent;
|
||||||
|
newSibling.insertAdjacentElement('beforebegin', elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return !(elem.nextElementSibling === null && !parent);
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
const newSibling = elem.nextElementSibling || parent;
|
||||||
|
newSibling.insertAdjacentElement('afterend', elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next_book: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return book.nextElementSibling !== null;
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
const newList = book.nextElementSibling.querySelector('ul');
|
||||||
|
newList.prepend(elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prev_book: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return book.previousElementSibling !== null;
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
const newList = book.previousElementSibling.querySelector('ul');
|
||||||
|
newList.appendChild(elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next_chapter: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
const nextChapter = this.getNextChapter(elem, parent);
|
||||||
|
nextChapter.querySelector('ul').prepend(elem);
|
||||||
|
},
|
||||||
|
getNextChapter(elem, parent) {
|
||||||
|
const topLevel = (parent || elem);
|
||||||
|
const topItems = Array.from(topLevel.parentElement.children);
|
||||||
|
const index = topItems.indexOf(topLevel);
|
||||||
|
return topItems.slice(index + 1).find(elem => elem.dataset.type === 'chapter');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prev_chapter: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
const prevChapter = this.getPrevChapter(elem, parent);
|
||||||
|
prevChapter.querySelector('ul').append(elem);
|
||||||
|
},
|
||||||
|
getPrevChapter(elem, parent) {
|
||||||
|
const topLevel = (parent || elem);
|
||||||
|
const topItems = Array.from(topLevel.parentElement.children);
|
||||||
|
const index = topItems.indexOf(topLevel);
|
||||||
|
return topItems.slice(0, index).reverse().find(elem => elem.dataset.type === 'chapter');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
book_end: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return parent || (parent === null && elem.nextElementSibling);
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
book.querySelector('ul').append(elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
book_start: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return parent || (parent === null && elem.previousElementSibling);
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
book.querySelector('ul').prepend(elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
before_chapter: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return parent;
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
parent.insertAdjacentElement('beforebegin', elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
after_chapter: {
|
||||||
|
active(elem, parent, book) {
|
||||||
|
return parent;
|
||||||
|
},
|
||||||
|
run(elem, parent, book) {
|
||||||
|
parent.insertAdjacentElement('afterend', elem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export class BookSort extends Component {
|
export class BookSort extends Component {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -44,15 +151,34 @@ export class BookSort extends Component {
|
||||||
this.sortContainer = this.$refs.sortContainer;
|
this.sortContainer = this.$refs.sortContainer;
|
||||||
this.input = this.$refs.input;
|
this.input = this.$refs.input;
|
||||||
|
|
||||||
|
Sortable.mount(new MultiDrag());
|
||||||
|
|
||||||
const initialSortBox = this.container.querySelector('.sort-box');
|
const initialSortBox = this.container.querySelector('.sort-box');
|
||||||
this.setupBookSortable(initialSortBox);
|
this.setupBookSortable(initialSortBox);
|
||||||
this.setupSortPresets();
|
this.setupSortPresets();
|
||||||
|
this.setupMoveActions();
|
||||||
|
|
||||||
window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
|
window.$events.listen('entity-select-change', this.bookSelect.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the handlers for the preset sort type buttons.
|
* Set up the handlers for the item-level move buttons.
|
||||||
|
*/
|
||||||
|
setupMoveActions() {
|
||||||
|
// Handle move button click
|
||||||
|
this.container.addEventListener('click', event => {
|
||||||
|
if (event.target.matches('[data-move]')) {
|
||||||
|
const action = event.target.getAttribute('data-move');
|
||||||
|
const sortItem = event.target.closest('[data-id]');
|
||||||
|
this.runSortAction(sortItem, action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateMoveActionStateForAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the handlers for the preset sort type buttons.
|
||||||
*/
|
*/
|
||||||
setupSortPresets() {
|
setupSortPresets() {
|
||||||
let lastSort = '';
|
let lastSort = '';
|
||||||
|
@ -100,16 +226,19 @@ export class BookSort extends Component {
|
||||||
const newBookContainer = htmlToDom(resp.data);
|
const newBookContainer = htmlToDom(resp.data);
|
||||||
this.sortContainer.append(newBookContainer);
|
this.sortContainer.append(newBookContainer);
|
||||||
this.setupBookSortable(newBookContainer);
|
this.setupBookSortable(newBookContainer);
|
||||||
|
this.updateMoveActionStateForAll();
|
||||||
|
|
||||||
|
const summary = newBookContainer.querySelector('summary');
|
||||||
|
summary.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the given book container element to have sortable items.
|
* Set up the given book container element to have sortable items.
|
||||||
* @param {Element} bookContainer
|
* @param {Element} bookContainer
|
||||||
*/
|
*/
|
||||||
setupBookSortable(bookContainer) {
|
setupBookSortable(bookContainer) {
|
||||||
const sortElems = [bookContainer.querySelector('.sort-list')];
|
const sortElems = Array.from(bookContainer.querySelectorAll('.sort-list, .sortable-page-sublist'));
|
||||||
sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul'));
|
|
||||||
|
|
||||||
const bookGroupConfig = {
|
const bookGroupConfig = {
|
||||||
name: 'book',
|
name: 'book',
|
||||||
|
@ -125,22 +254,40 @@ export class BookSort extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let sortElem of sortElems) {
|
for (const sortElem of sortElems) {
|
||||||
new Sortable(sortElem, {
|
Sortable.create(sortElem, {
|
||||||
group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
|
group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
|
||||||
animation: 150,
|
animation: 150,
|
||||||
fallbackOnBody: true,
|
fallbackOnBody: true,
|
||||||
swapThreshold: 0.65,
|
swapThreshold: 0.65,
|
||||||
onSort: this.updateMapInput.bind(this),
|
onSort: (event) => {
|
||||||
|
this.ensureNoNestedChapters()
|
||||||
|
this.updateMapInput();
|
||||||
|
this.updateMoveActionStateForAll();
|
||||||
|
},
|
||||||
dragClass: 'bg-white',
|
dragClass: 'bg-white',
|
||||||
ghostClass: 'primary-background-light',
|
ghostClass: 'primary-background-light',
|
||||||
multiDrag: true,
|
multiDrag: true,
|
||||||
multiDragKey: 'CTRL',
|
multiDragKey: 'Control',
|
||||||
selectedClass: 'sortable-selected',
|
selectedClass: 'sortable-selected',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle nested chapters by moving them to the parent book.
|
||||||
|
* Needed since sorting with multi-sort only checks group rules based on the active item,
|
||||||
|
* not all in group, therefore need to manually check after a sort.
|
||||||
|
* Must be done before updating the map input.
|
||||||
|
*/
|
||||||
|
ensureNoNestedChapters() {
|
||||||
|
const nestedChapters = this.container.querySelectorAll('[data-type="chapter"] [data-type="chapter"]');
|
||||||
|
for (const chapter of nestedChapters) {
|
||||||
|
const parentChapter = chapter.parentElement.closest('[data-type="chapter"]');
|
||||||
|
parentChapter.insertAdjacentElement('afterend', chapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the input with our sort data.
|
* Update the input with our sort data.
|
||||||
*/
|
*/
|
||||||
|
@ -202,4 +349,38 @@ export class BookSort extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the given sort action up the provided sort item.
|
||||||
|
* @param {Element} item
|
||||||
|
* @param {String} action
|
||||||
|
*/
|
||||||
|
runSortAction(item, action) {
|
||||||
|
const parentItem = item.parentElement.closest('li[data-id]');
|
||||||
|
const parentBook = item.parentElement.closest('[data-type="book"]');
|
||||||
|
moveActions[action].run(item, parentItem, parentBook);
|
||||||
|
this.updateMapInput();
|
||||||
|
this.updateMoveActionStateForAll();
|
||||||
|
item.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||||
|
item.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state of the available move actions on this item.
|
||||||
|
* @param {Element} item
|
||||||
|
*/
|
||||||
|
updateMoveActionState(item) {
|
||||||
|
const parentItem = item.parentElement.closest('li[data-id]');
|
||||||
|
const parentBook = item.parentElement.closest('[data-type="book"]');
|
||||||
|
for (const [action, functions] of Object.entries(moveActions)) {
|
||||||
|
const moveButton = item.querySelector(`[data-move="${action}"]`);
|
||||||
|
moveButton.disabled = !functions.active(item, parentItem, parentBook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMoveActionStateForAll() {
|
||||||
|
const items = this.container.querySelectorAll('[data-type="chapter"],[data-type="page"]');
|
||||||
|
for (const item of items) {
|
||||||
|
this.updateMoveActionState(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -15,7 +15,6 @@ export class EntitySelector extends Component {
|
||||||
this.searchInput = this.$refs.search;
|
this.searchInput = this.$refs.search;
|
||||||
this.loading = this.$refs.loading;
|
this.loading = this.$refs.loading;
|
||||||
this.resultsContainer = this.$refs.results;
|
this.resultsContainer = this.$refs.results;
|
||||||
this.addButton = this.$refs.add;
|
|
||||||
|
|
||||||
this.search = '';
|
this.search = '';
|
||||||
this.lastClick = 0;
|
this.lastClick = 0;
|
||||||
|
@ -43,15 +42,6 @@ export class EntitySelector extends Component {
|
||||||
if (event.keyCode === 13) event.preventDefault();
|
if (event.keyCode === 13) event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.addButton) {
|
|
||||||
this.addButton.addEventListener('click', event => {
|
|
||||||
if (this.selectedItemData) {
|
|
||||||
this.confirmSelection(this.selectedItemData);
|
|
||||||
this.unselectAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
|
onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
|
||||||
if (e.ctrlKey && e.code === 'Enter') {
|
if (e.ctrlKey && e.code === 'Enter') {
|
||||||
|
|
|
@ -86,7 +86,7 @@ export class KeyboardNavigationHandler {
|
||||||
*/
|
*/
|
||||||
#getFocusable() {
|
#getFocusable() {
|
||||||
const focusable = [];
|
const focusable = [];
|
||||||
const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"]),input:not([type=hidden])';
|
const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
|
||||||
for (const container of this.containers) {
|
for (const container of this.containers) {
|
||||||
focusable.push(...container.querySelectorAll(selector))
|
focusable.push(...container.querySelectorAll(selector))
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,6 +141,7 @@ return [
|
||||||
'books_search_this' => 'Search this book',
|
'books_search_this' => 'Search this book',
|
||||||
'books_navigation' => 'Book Navigation',
|
'books_navigation' => 'Book Navigation',
|
||||||
'books_sort' => 'Sort Book Contents',
|
'books_sort' => 'Sort Book Contents',
|
||||||
|
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.',
|
||||||
'books_sort_named' => 'Sort Book :bookName',
|
'books_sort_named' => 'Sort Book :bookName',
|
||||||
'books_sort_name' => 'Sort by Name',
|
'books_sort_name' => 'Sort by Name',
|
||||||
'books_sort_created' => 'Sort by Created Date',
|
'books_sort_created' => 'Sort by Created Date',
|
||||||
|
@ -149,6 +150,17 @@ return [
|
||||||
'books_sort_chapters_last' => 'Chapters Last',
|
'books_sort_chapters_last' => 'Chapters Last',
|
||||||
'books_sort_show_other' => 'Show Other Books',
|
'books_sort_show_other' => 'Show Other Books',
|
||||||
'books_sort_save' => 'Save New Order',
|
'books_sort_save' => 'Save New Order',
|
||||||
|
'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',
|
||||||
|
'books_sort_move_up' => 'Move Up',
|
||||||
|
'books_sort_move_down' => 'Move Down',
|
||||||
|
'books_sort_move_prev_book' => 'Move to Previous Book',
|
||||||
|
'books_sort_move_next_book' => 'Move to Next Book',
|
||||||
|
'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',
|
||||||
|
'books_sort_move_next_chapter' => 'Move Into Next Chapter',
|
||||||
|
'books_sort_move_book_start' => 'Move to Start of Book',
|
||||||
|
'books_sort_move_book_end' => 'Move to End of Book',
|
||||||
|
'books_sort_move_before_chapter' => 'Move to Before Chapter',
|
||||||
|
'books_sort_move_after_chapter' => 'Move to After Chapter',
|
||||||
'books_copy' => 'Copy Book',
|
'books_copy' => 'Copy Book',
|
||||||
'books_copy_success' => 'Book successfully copied',
|
'books_copy_success' => 'Book successfully copied',
|
||||||
|
|
||||||
|
|
|
@ -268,6 +268,11 @@ body.flexbox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky-top-m {
|
||||||
|
position: sticky;
|
||||||
|
top: $-m;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visibility
|
* Visibility
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -232,7 +232,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sortable Lists
|
// Sortable Lists
|
||||||
.sortable-page-list, .sortable-page-list ul {
|
.sortable-page-list, .sortable-page-sublist {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.sort-box {
|
.sort-box {
|
||||||
|
@ -267,7 +267,7 @@
|
||||||
.entity-list-item > span:first-child {
|
.entity-list-item > span:first-child {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
.sortable-selected .entity-list-item, .sortable-selected .entity-list-item:hover {
|
.sortable-selected, .sortable-selected:hover {
|
||||||
outline: 1px dotted var(--color-primary);
|
outline: 1px dotted var(--color-primary);
|
||||||
background-color: var(--color-primary-light) !important;
|
background-color: var(--color-primary-light) !important;
|
||||||
}
|
}
|
||||||
|
@ -278,12 +278,13 @@
|
||||||
> ul {
|
> ul {
|
||||||
margin-inline-start: 0;
|
margin-inline-start: 0;
|
||||||
}
|
}
|
||||||
ul {
|
.sortable-page-sublist {
|
||||||
margin-bottom: $-m;
|
margin-bottom: $-m;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-inline-start: $-m;
|
padding-inline-start: $-m;
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
|
@include lightDark(background-color, #FFF, #222);
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
@include lightDark(border-color, #DDD, #666);
|
@include lightDark(border-color, #DDD, #666);
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
|
@ -302,6 +303,36 @@
|
||||||
.sortable-page-list li.placeholder:before {
|
.sortable-page-list li.placeholder:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
.sort-box summary {
|
||||||
|
list-style: none;
|
||||||
|
font-size: .9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sort-box summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
details.sort-box summary .caret-container svg {
|
||||||
|
transition: transform ease-in-out 120ms;
|
||||||
|
}
|
||||||
|
details.sort-box[open] summary .caret-container svg {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.sort-box-actions .icon-button {
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
.sort-box .flex-container-row:hover .sort-box-actions .icon-button,
|
||||||
|
.sort-box .flex-container-row:focus-within .sort-box-actions .icon-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.sort-box-actions .icon-button[disabled] {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.sort-box-actions .dropdown-menu button[disabled] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.sort-list-handle {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
.activity-list-item {
|
.activity-list-item {
|
||||||
padding: $-s 0;
|
padding: $-s 0;
|
||||||
|
|
|
@ -187,18 +187,14 @@ $loadingSize: 10px;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
padding-top: $-l;
|
padding-top: $-l;
|
||||||
}
|
}
|
||||||
.entity-selector-add button {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
border-top: 1px solid #DDD;
|
|
||||||
}
|
|
||||||
&.compact {
|
&.compact {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
.entity-item-snippet {
|
.entity-item-snippet {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
resources/views/books/parts/sort-box-actions.blade.php
Normal file
26
resources/views/books/parts/sort-box-actions.blade.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<div class="sort-box-actions flex-container-row items-center px-s gap-xxs">
|
||||||
|
<button type="button" data-move="up" class="icon-button p-xs text-bigger"
|
||||||
|
title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
|
||||||
|
<button type="button" data-move="down" class="icon-button p-xs text-bigger"
|
||||||
|
title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
|
||||||
|
<div class="dropdown-container" component="dropdown">
|
||||||
|
<button refs="dropdown@toggle"
|
||||||
|
type="button"
|
||||||
|
title="{{ trans('common.more') }}"
|
||||||
|
class="icon-button p-xs text-bigger"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false">
|
||||||
|
@icon('more')
|
||||||
|
</button>
|
||||||
|
<div refs="dropdown@menu" class="dropdown-menu" role="menu">
|
||||||
|
<button type="button" class="text-item" data-move="prev_book">{{ trans('entities.books_sort_move_prev_book') }}</button>
|
||||||
|
<button type="button" class="text-item" data-move="next_book">{{ trans('entities.books_sort_move_next_book') }}</button>
|
||||||
|
<button type="button" class="text-item" data-move="prev_chapter">{{ trans('entities.books_sort_move_prev_chapter') }}</button>
|
||||||
|
<button type="button" class="text-item" data-move="next_chapter">{{ trans('entities.books_sort_move_next_chapter') }}</button>
|
||||||
|
<button type="button" class="text-item" data-move="book_start">{{ trans('entities.books_sort_move_book_start') }}</button>
|
||||||
|
<button type="button" class="text-item" data-move="book_end">{{ trans('entities.books_sort_move_book_end') }}</button>
|
||||||
|
<button type="button" class="text-item" data-move="before_chapter">{{ trans('entities.books_sort_move_before_chapter') }}</button>
|
||||||
|
<button type="button" class="text-item" data-move="after_chapter">{{ trans('entities.books_sort_move_after_chapter') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,8 +1,15 @@
|
||||||
<div class="sort-box" data-type="book" data-id="{{ $book->id }}">
|
<details class="sort-box" data-type="book" data-id="{{ $book->id }}" open>
|
||||||
<h5 class="text-book entity-list-item no-hover py-xs pl-none">
|
<summary>
|
||||||
<span>@icon('book')</span>
|
<h5 class="flex-container-row items-center justify-flex-start gap-xs">
|
||||||
<span>{{ $book->name }}</span>
|
<div class="text-book text-bigger caret-container">
|
||||||
</h5>
|
@icon('caret-right')
|
||||||
|
</div>
|
||||||
|
<div class="entity-list-item no-hover py-s text-book px-none">
|
||||||
|
<span>@icon('book')</span>
|
||||||
|
<span>{{ $book->name }}</span>
|
||||||
|
</div>
|
||||||
|
</h5>
|
||||||
|
</summary>
|
||||||
<div class="sort-box-options pb-sm">
|
<div class="sort-box-options pb-sm">
|
||||||
<button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button>
|
<button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button>
|
||||||
<button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button>
|
<button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button>
|
||||||
|
@ -14,29 +21,39 @@
|
||||||
|
|
||||||
@foreach($bookChildren as $bookChild)
|
@foreach($bookChildren as $bookChild)
|
||||||
<li class="text-{{ $bookChild->getType() }}"
|
<li class="text-{{ $bookChild->getType() }}"
|
||||||
data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getType() }}"
|
data-id="{{$bookChild->id}}"
|
||||||
data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}"
|
data-type="{{ $bookChild->getType() }}"
|
||||||
data-updated="{{ $bookChild->updated_at->timestamp }}">
|
data-name="{{ $bookChild->name }}"
|
||||||
<div class="entity-list-item">
|
data-created="{{ $bookChild->created_at->timestamp }}"
|
||||||
<span>@icon($bookChild->getType()) </span>
|
data-updated="{{ $bookChild->updated_at->timestamp }}"
|
||||||
<div>
|
tabindex="-1">
|
||||||
{{ $bookChild->name }}
|
<div class="flex-container-row items-center">
|
||||||
|
<div class="text-muted sort-list-handle px-s py-m">@icon('grip')</div>
|
||||||
|
<div class="entity-list-item px-none no-hover">
|
||||||
|
<span>@icon($bookChild->getType()) </span>
|
||||||
<div>
|
<div>
|
||||||
|
{{ $bookChild->name }}
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@include('books.parts.sort-box-actions')
|
||||||
</div>
|
</div>
|
||||||
@if($bookChild->isA('chapter'))
|
@if($bookChild->isA('chapter'))
|
||||||
<ul>
|
<ul class="sortable-page-sublist">
|
||||||
@foreach($bookChild->visible_pages as $page)
|
@foreach($bookChild->visible_pages as $page)
|
||||||
<li class="text-page"
|
<li class="text-page flex-container-row items-center"
|
||||||
data-id="{{$page->id}}" data-type="page"
|
data-id="{{$page->id}}" data-type="page"
|
||||||
data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}"
|
data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}"
|
||||||
data-updated="{{ $page->updated_at->timestamp }}">
|
data-updated="{{ $page->updated_at->timestamp }}"
|
||||||
<div class="entity-list-item">
|
tabindex="-1">
|
||||||
|
<div class="text-muted sort-list-handle px-s py-m">@icon('grip')</div>
|
||||||
|
<div class="entity-list-item px-none no-hover">
|
||||||
<span>@icon('page')</span>
|
<span>@icon('page')</span>
|
||||||
<span>{{ $page->name }}</span>
|
<span>{{ $page->name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@include('books.parts.sort-box-actions')
|
||||||
</li>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -45,4 +62,4 @@
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</details>
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
<div class="grid left-focus gap-xl">
|
<div class="grid left-focus gap-xl">
|
||||||
<div>
|
<div>
|
||||||
<div component="book-sort" class="card content-wrap">
|
<div component="book-sort" class="card content-wrap auto-height">
|
||||||
<h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
|
<h1 class="list-heading">{{ trans('entities.books_sort') }}</h1>
|
||||||
|
<p class="text-muted">{{ trans('entities.books_sort_desc') }}</p>
|
||||||
|
|
||||||
<div refs="book-sort@sortContainer">
|
<div refs="book-sort@sortContainer">
|
||||||
@include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
|
@include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,10 +37,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<main class="card content-wrap">
|
<main class="card content-wrap auto-height sticky-top-m">
|
||||||
<h2 class="list-heading mb-m">{{ trans('entities.books_sort_show_other') }}</h2>
|
<h2 class="list-heading">{{ trans('entities.books_sort_show_other') }}</h2>
|
||||||
|
<p class="text-muted">{{ trans('entities.books_sort_show_other_desc') }}</p>
|
||||||
|
|
||||||
@include('entities.selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update', 'showAdd' => true])
|
@include('entities.selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update'])
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,11 +8,5 @@
|
||||||
<input refs="entity-selector@search" type="text" placeholder="{{ trans('common.search') }}" @if($autofocus ?? false) autofocus @endif>
|
<input refs="entity-selector@search" type="text" placeholder="{{ trans('common.search') }}" @if($autofocus ?? false) autofocus @endif>
|
||||||
<div class="text-center loading" refs="entity-selector@loading">@include('common.loading-icon')</div>
|
<div class="text-center loading" refs="entity-selector@loading">@include('common.loading-icon')</div>
|
||||||
<div refs="entity-selector@results"></div>
|
<div refs="entity-selector@results"></div>
|
||||||
@if($showAdd ?? false)
|
|
||||||
<div class="entity-selector-add">
|
|
||||||
<button refs="entity-selector@add" type="button"
|
|
||||||
class="button outline">@icon('add'){{ trans('common.add') }}</button>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
Loading…
Add table
Reference in a new issue