From a3e7e754b94a77e6608e1bd35316f849c514a8e5 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 27 Jan 2023 11:16:17 +0000 Subject: [PATCH 1/6] Improves sortable ux - Fixes multi-select functionality. - Updated other books to be sticky. - Added some general intro/desc text. - Updated sort boxes to be collapsible. - Cleaned up other books styling. --- resources/js/components/book-sort.js | 12 +++++++----- resources/lang/en/entities.php | 2 ++ resources/sass/_layout.scss | 5 +++++ resources/sass/_lists.scss | 14 ++++++++++++++ resources/sass/styles.scss | 3 +++ .../views/books/parts/sort-box.blade.php | 19 +++++++++++++------ resources/views/books/sort.blade.php | 11 +++++++---- 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 3ffadf991..2722eb586 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -1,4 +1,4 @@ -import Sortable from "sortablejs"; +import Sortable, {MultiDrag} from "sortablejs"; import {Component} from "./component"; import {htmlToDom} from "../services/dom"; @@ -44,6 +44,8 @@ export class BookSort extends Component { this.sortContainer = this.$refs.sortContainer; this.input = this.$refs.input; + Sortable.mount(new MultiDrag()); + const initialSortBox = this.container.querySelector('.sort-box'); this.setupBookSortable(initialSortBox); this.setupSortPresets(); @@ -104,7 +106,7 @@ export class BookSort extends Component { } /** - * Setup the given book container element to have sortable items. + * Set up the given book container element to have sortable items. * @param {Element} bookContainer */ setupBookSortable(bookContainer) { @@ -125,8 +127,8 @@ export class BookSort extends Component { } }; - for (let sortElem of sortElems) { - new Sortable(sortElem, { + for (const sortElem of sortElems) { + Sortable.create(sortElem, { group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig, animation: 150, fallbackOnBody: true, @@ -135,7 +137,7 @@ export class BookSort extends Component { dragClass: 'bg-white', ghostClass: 'primary-background-light', multiDrag: true, - multiDragKey: 'CTRL', + multiDragKey: 'Control', selectedClass: 'sortable-selected', }); } diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index fa2586f8d..5b019e848 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -141,6 +141,7 @@ return [ 'books_search_this' => 'Search this book', 'books_navigation' => 'Book Navigation', '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_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', @@ -148,6 +149,7 @@ return [ 'books_sort_chapters_first' => 'Chapters First', 'books_sort_chapters_last' => 'Chapters Last', 'books_sort_show_other' => 'Show Other Books', + 'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.', 'books_sort_save' => 'Save New Order', 'books_copy' => 'Copy Book', 'books_copy_success' => 'Book successfully copied', diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 4c7de600b..3fc419046 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -268,6 +268,11 @@ body.flexbox { } } +.sticky-top-m { + position: sticky; + top: $-m; +} + /** * Visibility */ diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 86a89051f..1ae801267 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -302,6 +302,20 @@ .sortable-page-list li.placeholder:before { 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); +} .activity-list-item { padding: $-s 0; diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 23959d1f8..398d16fac 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -199,6 +199,9 @@ $loadingSize: 10px; .entity-item-snippet { display: none; } + h4 { + font-size: 14px; + } } } diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index ef9929e46..819f1e063 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -1,8 +1,15 @@ -<div class="sort-box" data-type="book" data-id="{{ $book->id }}"> - <h5 class="text-book entity-list-item no-hover py-xs pl-none"> - <span>@icon('book')</span> - <span>{{ $book->name }}</span> - </h5> +<details class="sort-box" data-type="book" data-id="{{ $book->id }}" open> + <summary> + <h5 class="flex-container-row items-center justify-flex-start gap-xs"> + <div class="text-book text-bigger caret-container"> + @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"> <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> @@ -45,4 +52,4 @@ @endforeach </ul> -</div> \ No newline at end of file +</details> \ No newline at end of file diff --git a/resources/views/books/sort.blade.php b/resources/views/books/sort.blade.php index 077da101d..b778398a8 100644 --- a/resources/views/books/sort.blade.php +++ b/resources/views/books/sort.blade.php @@ -16,8 +16,10 @@ <div class="grid left-focus gap-xl"> <div> - <div component="book-sort" class="card content-wrap"> - <h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1> + <div component="book-sort" class="card content-wrap auto-height"> + <h1 class="list-heading">{{ trans('entities.books_sort') }}</h1> + <p class="text-muted">{{ trans('entities.books_sort_desc') }}</p> + <div refs="book-sort@sortContainer"> @include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]) </div> @@ -35,8 +37,9 @@ </div> <div> - <main class="card content-wrap"> - <h2 class="list-heading mb-m">{{ trans('entities.books_sort_show_other') }}</h2> + <main class="card content-wrap auto-height sticky-top-m"> + <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]) From 7cacbaadf0dc2d9b94e163597bd2b3d2cc05be53 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 27 Jan 2023 13:08:35 +0000 Subject: [PATCH 2/6] Added functionality/logic for button-based sorting --- resources/js/components/book-sort.js | 167 ++++++++++++++++++ .../books/parts/sort-box-actions.blade.php | 12 ++ .../views/books/parts/sort-box.blade.php | 9 +- 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 resources/views/books/parts/sort-box-actions.blade.php diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 2722eb586..3c849c5c6 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -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 { setup() { @@ -49,10 +156,35 @@ export class BookSort extends Component { const initialSortBox = this.container.querySelector('.sort-box'); this.setupBookSortable(initialSortBox); this.setupSortPresets(); + this.setupMoveActions(); window.$events.listen('entity-select-confirm', this.bookSelect.bind(this)); } + /** + * Setup 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); + } + }); + // TODO - Probably can remove this + // // Handle action updating on likely use + // this.container.addEventListener('focusin', event => { + // const sortItem = event.target.closest('[data-type="chapter"],[data-type="page"]'); + // if (sortItem) { + // this.updateMoveActionState(sortItem); + // } + // }); + + this.updateMoveActionStateForAll(); + } + /** * Setup the handlers for the preset sort type buttons. */ @@ -102,6 +234,7 @@ export class BookSort extends Component { const newBookContainer = htmlToDom(resp.data); this.sortContainer.append(newBookContainer); this.setupBookSortable(newBookContainer); + this.updateMoveActionStateForAll(); }); } @@ -204,4 +337,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); + } + } } \ No newline at end of file diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php new file mode 100644 index 000000000..0c91f42da --- /dev/null +++ b/resources/views/books/parts/sort-box-actions.blade.php @@ -0,0 +1,12 @@ +<div class="sort-box-actions"> + <button type="button" data-move="up">Move Up</button> + <button type="button" data-move="down">Move Down</button> + <button type="button" data-move="prev_book">Move To Previous Book</button> + <button type="button" data-move="next_book">Move To Next Book</button> + <button type="button" data-move="prev_chapter">Move Into Previous Chapter</button> + <button type="button" data-move="next_chapter">Move Into Next Chapter</button> + <button type="button" data-move="book_start">Move To Start of Book</button> + <button type="button" data-move="book_end">Move To End of Book</button> + <button type="button" data-move="before_chapter">Move To Before Chapter</button> + <button type="button" data-move="after_chapter">Move To After Chapter</button> +</div> \ No newline at end of file diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index 819f1e063..77a03f831 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -23,7 +23,8 @@ <li class="text-{{ $bookChild->getType() }}" data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getType() }}" data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}" - data-updated="{{ $bookChild->updated_at->timestamp }}"> + data-updated="{{ $bookChild->updated_at->timestamp }}" tabindex="0"> + <div class="text-muted sort-list-handle">@icon('grip')</div> <div class="entity-list-item"> <span>@icon($bookChild->getType()) </span> <div> @@ -33,17 +34,21 @@ </div> </div> </div> + @include('books.parts.sort-box-actions') @if($bookChild->isA('chapter')) <ul> @foreach($bookChild->visible_pages as $page) <li class="text-page" data-id="{{$page->id}}" data-type="page" data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}" - data-updated="{{ $page->updated_at->timestamp }}"> + data-updated="{{ $page->updated_at->timestamp }}" + tabindex="0"> + <div class="text-muted sort-list-handle">@icon('grip')</div> <div class="entity-list-item"> <span>@icon('page')</span> <span>{{ $page->name }}</span> </div> + @include('books.parts.sort-box-actions') </li> @endforeach </ul> From 40e112fc5b4896c52d5aa09ea2ad2a9da6eccfa1 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 27 Jan 2023 13:26:58 +0000 Subject: [PATCH 3/6] Extracted text & added dropdown for book sort move actions Primarily styling and testing left to do. --- resources/lang/en/entities.php | 11 ++++++- resources/sass/_lists.scss | 4 +-- .../books/parts/sort-box-actions.blade.php | 31 +++++++++++++------ .../views/books/parts/sort-box.blade.php | 2 +- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 5b019e848..834cfacba 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -150,7 +150,16 @@ return [ 'books_sort_chapters_last' => 'Chapters Last', 'books_sort_show_other' => 'Show Other Books', 'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.', - 'books_sort_save' => 'Save New Order', + '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_success' => 'Book successfully copied', diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 1ae801267..39b2afee6 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -232,7 +232,7 @@ } // Sortable Lists -.sortable-page-list, .sortable-page-list ul { +.sortable-page-list, .sortable-page-sublist { list-style: none; } .sort-box { @@ -278,7 +278,7 @@ > ul { margin-inline-start: 0; } - ul { + .sortable-page-sublist { margin-bottom: $-m; margin-top: 0; padding-inline-start: $-m; diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php index 0c91f42da..c9a1d323e 100644 --- a/resources/views/books/parts/sort-box-actions.blade.php +++ b/resources/views/books/parts/sort-box-actions.blade.php @@ -1,12 +1,23 @@ <div class="sort-box-actions"> - <button type="button" data-move="up">Move Up</button> - <button type="button" data-move="down">Move Down</button> - <button type="button" data-move="prev_book">Move To Previous Book</button> - <button type="button" data-move="next_book">Move To Next Book</button> - <button type="button" data-move="prev_chapter">Move Into Previous Chapter</button> - <button type="button" data-move="next_chapter">Move Into Next Chapter</button> - <button type="button" data-move="book_start">Move To Start of Book</button> - <button type="button" data-move="book_end">Move To End of Book</button> - <button type="button" data-move="before_chapter">Move To Before Chapter</button> - <button type="button" data-move="after_chapter">Move To After Chapter</button> + <button type="button" data-move="up" title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button> + <button type="button" data-move="down" + title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button> + <div class="dropdown-container" component="dropdown"> + <button refs="dropdown@toggle" + title="{{ trans('common.more') }}" + 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> \ No newline at end of file diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index 77a03f831..33448f483 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -36,7 +36,7 @@ </div> @include('books.parts.sort-box-actions') @if($bookChild->isA('chapter')) - <ul> + <ul class="sortable-page-sublist"> @foreach($bookChild->visible_pages as $page) <li class="text-page" data-id="{{$page->id}}" data-type="page" From 022cbb9c004561c902bfea86221424cf2fdb03ac Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 27 Jan 2023 16:25:06 +0000 Subject: [PATCH 4/6] Finished off design and fixing of sort buttons --- resources/js/components/book-sort.js | 18 ++++-------- resources/js/services/keyboard-navigation.js | 2 +- resources/lang/en/entities.php | 13 +++++---- resources/sass/_lists.scss | 19 +++++++++++- .../books/parts/sort-box-actions.blade.php | 8 +++-- .../views/books/parts/sort-box.blade.php | 29 +++++++++++-------- 6 files changed, 54 insertions(+), 35 deletions(-) diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 3c849c5c6..e8ecd49a4 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -162,7 +162,7 @@ export class BookSort extends Component { } /** - * Setup the handlers for the item-level move buttons. + * Set up the handlers for the item-level move buttons. */ setupMoveActions() { // Handle move button click @@ -173,20 +173,12 @@ export class BookSort extends Component { this.runSortAction(sortItem, action); } }); - // TODO - Probably can remove this - // // Handle action updating on likely use - // this.container.addEventListener('focusin', event => { - // const sortItem = event.target.closest('[data-type="chapter"],[data-type="page"]'); - // if (sortItem) { - // this.updateMoveActionState(sortItem); - // } - // }); this.updateMoveActionStateForAll(); } /** - * Setup the handlers for the preset sort type buttons. + * Set up the handlers for the preset sort type buttons. */ setupSortPresets() { let lastSort = ''; @@ -235,6 +227,9 @@ export class BookSort extends Component { this.sortContainer.append(newBookContainer); this.setupBookSortable(newBookContainer); this.updateMoveActionStateForAll(); + + const summary = newBookContainer.querySelector('summary'); + summary.focus(); }); } @@ -243,8 +238,7 @@ export class BookSort extends Component { * @param {Element} bookContainer */ setupBookSortable(bookContainer) { - const sortElems = [bookContainer.querySelector('.sort-list')]; - sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul')); + const sortElems = Array.from(bookContainer.querySelectorAll('.sort-list, .sortable-page-sublist')); const bookGroupConfig = { name: 'book', diff --git a/resources/js/services/keyboard-navigation.js b/resources/js/services/keyboard-navigation.js index 0e1dcf1a7..0f866ceaa 100644 --- a/resources/js/services/keyboard-navigation.js +++ b/resources/js/services/keyboard-navigation.js @@ -86,7 +86,7 @@ export class KeyboardNavigationHandler { */ #getFocusable() { 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) { focusable.push(...container.querySelectorAll(selector)) } diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 834cfacba..8bf805774 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -149,17 +149,18 @@ return [ 'books_sort_chapters_first' => 'Chapters First', 'books_sort_chapters_last' => 'Chapters Last', 'books_sort_show_other' => 'Show Other Books', + '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_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_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_success' => 'Book successfully copied', diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 39b2afee6..33e500d6a 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -267,7 +267,7 @@ .entity-list-item > span:first-child { 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); background-color: var(--color-primary-light) !important; } @@ -284,6 +284,7 @@ padding-inline-start: $-m; } li { + @include lightDark(background-color, #FFF, #222); border: 1px solid; @include lightDark(border-color, #DDD, #666); margin-top: -1px; @@ -316,6 +317,22 @@ details.sort-box summary .caret-container svg { 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 { padding: $-s 0; diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php index c9a1d323e..cd5a59e15 100644 --- a/resources/views/books/parts/sort-box-actions.blade.php +++ b/resources/views/books/parts/sort-box-actions.blade.php @@ -1,10 +1,12 @@ -<div class="sort-box-actions"> - <button type="button" data-move="up" title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button> - <button type="button" data-move="down" +<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" title="{{ trans('common.more') }}" + class="icon-button p-xs text-bigger" aria-haspopup="true" aria-expanded="false"> @icon('more') diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index 33448f483..ca034a880 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -21,30 +21,35 @@ @foreach($bookChildren as $bookChild) <li class="text-{{ $bookChild->getType() }}" - data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getType() }}" - data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}" - data-updated="{{ $bookChild->updated_at->timestamp }}" tabindex="0"> - <div class="text-muted sort-list-handle">@icon('grip')</div> - <div class="entity-list-item"> - <span>@icon($bookChild->getType()) </span> - <div> - {{ $bookChild->name }} + data-id="{{$bookChild->id}}" + data-type="{{ $bookChild->getType() }}" + data-name="{{ $bookChild->name }}" + data-created="{{ $bookChild->created_at->timestamp }}" + data-updated="{{ $bookChild->updated_at->timestamp }}" + tabindex="0"> + <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> + {{ $bookChild->name }} + <div> + </div> </div> </div> + @include('books.parts.sort-box-actions') </div> - @include('books.parts.sort-box-actions') @if($bookChild->isA('chapter')) <ul class="sortable-page-sublist"> @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-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}" data-updated="{{ $page->updated_at->timestamp }}" tabindex="0"> - <div class="text-muted sort-list-handle">@icon('grip')</div> - <div class="entity-list-item"> + <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>{{ $page->name }}</span> </div> From b649738718fb62632574530392299f9aa9bbf53b Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 27 Jan 2023 17:06:39 +0000 Subject: [PATCH 5/6] Made book-sort changes based on screen reader testing - Removed having sort items in tabbing order since they have no action. - Updated "show other books" list to add upon single selection since it was not clear how these were added (double press) without then seeing the add button, and even then the add button would be after the scroll list. --- resources/js/components/book-sort.js | 7 +++++-- resources/js/components/entity-selector.js | 10 ---------- resources/sass/styles.scss | 7 ------- resources/views/books/parts/sort-box-actions.blade.php | 1 + resources/views/books/parts/sort-box.blade.php | 4 ++-- resources/views/books/sort.blade.php | 2 +- resources/views/entities/selector.blade.php | 6 ------ 7 files changed, 9 insertions(+), 28 deletions(-) diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index e8ecd49a4..6e56e43a5 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -158,7 +158,7 @@ export class BookSort extends Component { this.setupSortPresets(); this.setupMoveActions(); - window.$events.listen('entity-select-confirm', this.bookSelect.bind(this)); + window.$events.listen('entity-select-change', this.bookSelect.bind(this)); } /** @@ -260,7 +260,10 @@ export class BookSort extends Component { animation: 150, fallbackOnBody: true, swapThreshold: 0.65, - onSort: this.updateMapInput.bind(this), + onSort: () => { + this.updateMapInput(); + this.updateMoveActionStateForAll(); + }, dragClass: 'bg-white', ghostClass: 'primary-background-light', multiDrag: true, diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js index 1384b33a9..09d14b233 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.js @@ -15,7 +15,6 @@ export class EntitySelector extends Component { this.searchInput = this.$refs.search; this.loading = this.$refs.loading; this.resultsContainer = this.$refs.results; - this.addButton = this.$refs.add; this.search = ''; this.lastClick = 0; @@ -43,15 +42,6 @@ export class EntitySelector extends Component { 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 onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => { if (e.ctrlKey && e.code === 'Enter') { diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 398d16fac..e50a2f96a 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -187,13 +187,6 @@ $loadingSize: 10px; height: 400px; padding-top: $-l; } - .entity-selector-add button { - margin: 0; - display: block; - width: 100%; - border: 0; - border-top: 1px solid #DDD; - } &.compact { font-size: 10px; .entity-item-snippet { diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php index cd5a59e15..3796ffafb 100644 --- a/resources/views/books/parts/sort-box-actions.blade.php +++ b/resources/views/books/parts/sort-box-actions.blade.php @@ -5,6 +5,7 @@ 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" diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index ca034a880..03998e261 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -26,7 +26,7 @@ data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}" data-updated="{{ $bookChild->updated_at->timestamp }}" - tabindex="0"> + tabindex="-1"> <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"> @@ -47,7 +47,7 @@ data-id="{{$page->id}}" data-type="page" data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}" data-updated="{{ $page->updated_at->timestamp }}" - tabindex="0"> + 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> diff --git a/resources/views/books/sort.blade.php b/resources/views/books/sort.blade.php index b778398a8..c82ad4e3b 100644 --- a/resources/views/books/sort.blade.php +++ b/resources/views/books/sort.blade.php @@ -41,7 +41,7 @@ <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> </div> diff --git a/resources/views/entities/selector.blade.php b/resources/views/entities/selector.blade.php index 45fae4707..a9f5b932c 100644 --- a/resources/views/entities/selector.blade.php +++ b/resources/views/entities/selector.blade.php @@ -8,11 +8,5 @@ <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 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> \ No newline at end of file From 87e371ffde35043b8d889d4012fee0fbdf5a0e36 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 27 Jan 2023 17:39:51 +0000 Subject: [PATCH 6/6] Added prevention of nested chapters on sort --- resources/js/components/book-sort.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 6e56e43a5..5ae283fd0 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -260,7 +260,8 @@ export class BookSort extends Component { animation: 150, fallbackOnBody: true, swapThreshold: 0.65, - onSort: () => { + onSort: (event) => { + this.ensureNoNestedChapters() this.updateMapInput(); this.updateMoveActionStateForAll(); }, @@ -273,6 +274,20 @@ export class BookSort extends Component { } } + /** + * 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. */