]> BookStack Code Mirror - bookstack/commitdiff
Added functionality/logic for button-based sorting
authorDan Brown <redacted>
Fri, 27 Jan 2023 13:08:35 +0000 (13:08 +0000)
committerDan Brown <redacted>
Fri, 27 Jan 2023 13:08:35 +0000 (13:08 +0000)
resources/js/components/book-sort.js
resources/views/books/parts/sort-box-actions.blade.php [new file with mode: 0644]
resources/views/books/parts/sort-box.blade.php

index 2722eb586f5c3d3d108f889ec39a416a9271869c..3c849c5c6b7635503ebc539a8bd581a794a0c3a5 100644 (file)
@@ -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 (file)
index 0000000..0c91f42
--- /dev/null
@@ -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
index 819f1e063bfd6c10275e412fec0f8c19f6ebd760..77a03f83105dca95bfe53adecc05e1d024448508 100644 (file)
@@ -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>
                         </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>