-import Sortable from "sortablejs";
+import Sortable, {MultiDrag} from 'sortablejs';
+import {Component} from './component';
+import {htmlToDom} from '../services/dom.ts';
// Auto sort control
const sortOperations = {
- name: function(a, b) {
+ name(a, b) {
const aName = a.getAttribute('data-name').trim().toLowerCase();
const bName = b.getAttribute('data-name').trim().toLowerCase();
return aName.localeCompare(bName);
},
- created: function(a, b) {
+ created(a, b) {
const aTime = Number(a.getAttribute('data-created'));
const bTime = Number(b.getAttribute('data-created'));
return bTime - aTime;
},
- updated: function(a, b) {
+ updated(a, b) {
const aTime = Number(a.getAttribute('data-updated'));
const bTime = Number(b.getAttribute('data-updated'));
return bTime - aTime;
},
- chaptersFirst: function(a, b) {
+ chaptersFirst(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
}
return (aType === 'chapter' ? -1 : 1);
},
- chaptersLast: function(a, b) {
+ chaptersLast(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
},
};
-class BookSort {
+/**
+ * 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) {
+ return !(elem.previousElementSibling === null && !parent);
+ },
+ run(elem, parent) {
+ const newSibling = elem.previousElementSibling || parent;
+ newSibling.insertAdjacentElement('beforebegin', elem);
+ },
+ },
+ down: {
+ active(elem, parent) {
+ return !(elem.nextElementSibling === null && !parent);
+ },
+ run(elem, parent) {
+ 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) {
+ return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);
+ },
+ run(elem, parent) {
+ 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(item => item.dataset.type === 'chapter');
+ },
+ },
+ prev_chapter: {
+ active(elem, parent) {
+ return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);
+ },
+ run(elem, parent) {
+ 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(item => item.dataset.type === 'chapter');
+ },
+ },
+ book_end: {
+ active(elem, parent) {
+ return parent || (parent === null && elem.nextElementSibling);
+ },
+ run(elem, parent, book) {
+ book.querySelector('ul').append(elem);
+ },
+ },
+ book_start: {
+ active(elem, parent) {
+ return parent || (parent === null && elem.previousElementSibling);
+ },
+ run(elem, parent, book) {
+ book.querySelector('ul').prepend(elem);
+ },
+ },
+ before_chapter: {
+ active(elem, parent) {
+ return parent;
+ },
+ run(elem, parent) {
+ parent.insertAdjacentElement('beforebegin', elem);
+ },
+ },
+ after_chapter: {
+ active(elem, parent) {
+ return parent;
+ },
+ run(elem, parent) {
+ parent.insertAdjacentElement('afterend', elem);
+ },
+ },
+};
+
+export class BookSort extends Component {
+
+ setup() {
+ this.container = this.$el;
+ this.sortContainer = this.$refs.sortContainer;
+ this.input = this.$refs.input;
- constructor(elem) {
- this.elem = elem;
- this.sortContainer = elem.querySelector('[book-sort-boxes]');
- this.input = elem.querySelector('[book-sort-input]');
+ Sortable.mount(new MultiDrag());
- const initialSortBox = elem.querySelector('.sort-box');
+ const initialSortBox = this.container.querySelector('.sort-box');
this.setupBookSortable(initialSortBox);
this.setupSortPresets();
+ this.setupMoveActions();
+
+ window.$events.listen('entity-select-change', this.bookSelect.bind(this));
+ }
+
+ /**
+ * 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);
+ }
+ });
- window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
+ this.updateMoveActionStateForAll();
}
/**
- * Setup the handlers for the preset sort type buttons.
+ * Set up the handlers for the preset sort type buttons.
*/
setupSortPresets() {
let lastSort = '';
reverse = (lastSort === sort) ? !reverse : false;
let sortFunction = sortOperations[sort];
if (reverse && reversibleTypes.includes(sort)) {
- sortFunction = function(a, b) {
- return 0 - sortOperations[sort](a, b)
+ sortFunction = function reverseSortOperation(a, b) {
+ return 0 - sortOperations[sort](a, b);
};
}
- for (let list of sortLists) {
+ for (const list of sortLists) {
const directItems = Array.from(list.children).filter(child => child.matches('li'));
directItems.sort(sortFunction).forEach(sortedItem => {
list.appendChild(sortedItem);
* @param {Object} entityInfo
*/
bookSelect(entityInfo) {
- const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
+ const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
if (alreadyAdded) return;
- const entitySortItemUrl = entityInfo.link + '/sort-item';
+ const entitySortItemUrl = `${entityInfo.link}/sort-item`;
window.$http.get(entitySortItemUrl).then(resp => {
- const wrap = document.createElement('div');
- wrap.innerHTML = resp.data;
- const newBookContainer = wrap.children[0];
+ const newBookContainer = htmlToDom(resp.data);
this.sortContainer.append(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
*/
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',
const chapterGroupConfig = {
name: 'chapter',
pull: ['book', 'chapter'],
- put: function(toList, fromList, draggedElem) {
+ put(toList, fromList, draggedElem) {
return draggedElem.getAttribute('data-type') === 'page';
- }
+ },
};
- 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,
swapThreshold: 0.65,
- onSort: this.updateMapInput.bind(this),
+ onSort: () => {
+ this.ensureNoNestedChapters();
+ this.updateMapInput();
+ this.updateMoveActionStateForAll();
+ },
dragClass: 'bg-white',
ghostClass: 'primary-background-light',
+ multiDrag: true,
+ multiDragKey: 'Control',
+ 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.
*/
*/
buildEntityMap() {
const entityMap = [];
- const lists = this.elem.querySelectorAll('.sort-list');
+ const lists = this.container.querySelectorAll('.sort-list');
- for (let list of lists) {
+ for (const list of lists) {
const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
const directChildren = Array.from(list.children)
.filter(elem => elem.matches('[data-type="page"], [data-type="chapter"]'));
entityMap.push({
id: childId,
sort: index,
- parentChapter: parentChapter,
- type: type,
- book: bookId
+ parentChapter,
+ type,
+ book: bookId,
});
const subPages = childElem.querySelectorAll('[data-type="page"]');
sort: i,
parentChapter: childId,
type: 'page',
- book: bookId
+ book: bookId,
});
}
}
-}
+ /**
+ * 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);
+ }
+ }
-export default BookSort;
\ No newline at end of file
+ updateMoveActionStateForAll() {
+ const items = this.container.querySelectorAll('[data-type="chapter"],[data-type="page"]');
+ for (const item of items) {
+ this.updateMoveActionState(item);
+ }
+ }
+
+}