1 import Sortable, {MultiDrag} from "sortablejs";
2 import {Component} from "./component";
3 import {htmlToDom} from "../services/dom";
6 const sortOperations = {
8 const aName = a.getAttribute('data-name').trim().toLowerCase();
9 const bName = b.getAttribute('data-name').trim().toLowerCase();
10 return aName.localeCompare(bName);
12 created: function(a, b) {
13 const aTime = Number(a.getAttribute('data-created'));
14 const bTime = Number(b.getAttribute('data-created'));
17 updated: function(a, b) {
18 const aTime = Number(a.getAttribute('data-updated'));
19 const bTime = Number(b.getAttribute('data-updated'));
22 chaptersFirst: function(a, b) {
23 const aType = a.getAttribute('data-type');
24 const bType = b.getAttribute('data-type');
25 if (aType === bType) {
28 return (aType === 'chapter' ? -1 : 1);
30 chaptersLast: function(a, b) {
31 const aType = a.getAttribute('data-type');
32 const bType = b.getAttribute('data-type');
33 if (aType === bType) {
36 return (aType === 'chapter' ? 1 : -1);
41 * The available move actions.
42 * The active function indicates if the action is possible for the given item.
43 * The run function performs the move.
44 * @type {{up: {active(Element, ?Element, Element): boolean, run(Element, ?Element, Element)}}}
48 active(elem, parent, book) {
49 return !(elem.previousElementSibling === null && !parent);
51 run(elem, parent, book) {
52 const newSibling = elem.previousElementSibling || parent;
53 newSibling.insertAdjacentElement('beforebegin', elem);
57 active(elem, parent, book) {
58 return !(elem.nextElementSibling === null && !parent);
60 run(elem, parent, book) {
61 const newSibling = elem.nextElementSibling || parent;
62 newSibling.insertAdjacentElement('afterend', elem);
66 active(elem, parent, book) {
67 return book.nextElementSibling !== null;
69 run(elem, parent, book) {
70 const newList = book.nextElementSibling.querySelector('ul');
71 newList.prepend(elem);
75 active(elem, parent, book) {
76 return book.previousElementSibling !== null;
78 run(elem, parent, book) {
79 const newList = book.previousElementSibling.querySelector('ul');
80 newList.appendChild(elem);
84 active(elem, parent, book) {
85 return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);
87 run(elem, parent, book) {
88 const nextChapter = this.getNextChapter(elem, parent);
89 nextChapter.querySelector('ul').prepend(elem);
91 getNextChapter(elem, parent) {
92 const topLevel = (parent || elem);
93 const topItems = Array.from(topLevel.parentElement.children);
94 const index = topItems.indexOf(topLevel);
95 return topItems.slice(index + 1).find(elem => elem.dataset.type === 'chapter');
99 active(elem, parent, book) {
100 return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);
102 run(elem, parent, book) {
103 const prevChapter = this.getPrevChapter(elem, parent);
104 prevChapter.querySelector('ul').append(elem);
106 getPrevChapter(elem, parent) {
107 const topLevel = (parent || elem);
108 const topItems = Array.from(topLevel.parentElement.children);
109 const index = topItems.indexOf(topLevel);
110 return topItems.slice(0, index).reverse().find(elem => elem.dataset.type === 'chapter');
114 active(elem, parent, book) {
115 return parent || (parent === null && elem.nextElementSibling);
117 run(elem, parent, book) {
118 book.querySelector('ul').append(elem);
122 active(elem, parent, book) {
123 return parent || (parent === null && elem.previousElementSibling);
125 run(elem, parent, book) {
126 book.querySelector('ul').prepend(elem);
130 active(elem, parent, book) {
133 run(elem, parent, book) {
134 parent.insertAdjacentElement('beforebegin', elem);
138 active(elem, parent, book) {
141 run(elem, parent, book) {
142 parent.insertAdjacentElement('afterend', elem);
147 export class BookSort extends Component {
150 this.container = this.$el;
151 this.sortContainer = this.$refs.sortContainer;
152 this.input = this.$refs.input;
154 Sortable.mount(new MultiDrag());
156 const initialSortBox = this.container.querySelector('.sort-box');
157 this.setupBookSortable(initialSortBox);
158 this.setupSortPresets();
159 this.setupMoveActions();
161 window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
165 * Setup the handlers for the item-level move buttons.
168 // Handle move button click
169 this.container.addEventListener('click', event => {
170 if (event.target.matches('[data-move]')) {
171 const action = event.target.getAttribute('data-move');
172 const sortItem = event.target.closest('[data-id]');
173 this.runSortAction(sortItem, action);
176 // TODO - Probably can remove this
177 // // Handle action updating on likely use
178 // this.container.addEventListener('focusin', event => {
179 // const sortItem = event.target.closest('[data-type="chapter"],[data-type="page"]');
181 // this.updateMoveActionState(sortItem);
185 this.updateMoveActionStateForAll();
189 * Setup the handlers for the preset sort type buttons.
194 const reversibleTypes = ['name', 'created', 'updated'];
196 this.sortContainer.addEventListener('click', event => {
197 const sortButton = event.target.closest('.sort-box-options [data-sort]');
198 if (!sortButton) return;
200 event.preventDefault();
201 const sortLists = sortButton.closest('.sort-box').querySelectorAll('ul');
202 const sort = sortButton.getAttribute('data-sort');
204 reverse = (lastSort === sort) ? !reverse : false;
205 let sortFunction = sortOperations[sort];
206 if (reverse && reversibleTypes.includes(sort)) {
207 sortFunction = function(a, b) {
208 return 0 - sortOperations[sort](a, b)
212 for (let list of sortLists) {
213 const directItems = Array.from(list.children).filter(child => child.matches('li'));
214 directItems.sort(sortFunction).forEach(sortedItem => {
215 list.appendChild(sortedItem);
220 this.updateMapInput();
225 * Handle book selection from the entity selector.
226 * @param {Object} entityInfo
228 bookSelect(entityInfo) {
229 const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
230 if (alreadyAdded) return;
232 const entitySortItemUrl = entityInfo.link + '/sort-item';
233 window.$http.get(entitySortItemUrl).then(resp => {
234 const newBookContainer = htmlToDom(resp.data);
235 this.sortContainer.append(newBookContainer);
236 this.setupBookSortable(newBookContainer);
237 this.updateMoveActionStateForAll();
242 * Set up the given book container element to have sortable items.
243 * @param {Element} bookContainer
245 setupBookSortable(bookContainer) {
246 const sortElems = [bookContainer.querySelector('.sort-list')];
247 sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul'));
249 const bookGroupConfig = {
251 pull: ['book', 'chapter'],
252 put: ['book', 'chapter'],
255 const chapterGroupConfig = {
257 pull: ['book', 'chapter'],
258 put: function(toList, fromList, draggedElem) {
259 return draggedElem.getAttribute('data-type') === 'page';
263 for (const sortElem of sortElems) {
264 Sortable.create(sortElem, {
265 group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
267 fallbackOnBody: true,
269 onSort: this.updateMapInput.bind(this),
270 dragClass: 'bg-white',
271 ghostClass: 'primary-background-light',
273 multiDragKey: 'Control',
274 selectedClass: 'sortable-selected',
280 * Update the input with our sort data.
283 const pageMap = this.buildEntityMap();
284 this.input.value = JSON.stringify(pageMap);
288 * Build up a mapping of entities with their ordering and nesting.
292 const entityMap = [];
293 const lists = this.container.querySelectorAll('.sort-list');
295 for (let list of lists) {
296 const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
297 const directChildren = Array.from(list.children)
298 .filter(elem => elem.matches('[data-type="page"], [data-type="chapter"]'));
299 for (let i = 0; i < directChildren.length; i++) {
300 this.addBookChildToMap(directChildren[i], i, bookId, entityMap);
308 * Parse a sort item and add it to a data-map array.
309 * Parses sub0items if existing also.
310 * @param {Element} childElem
311 * @param {Number} index
312 * @param {Number} bookId
313 * @param {Array} entityMap
315 addBookChildToMap(childElem, index, bookId, entityMap) {
316 const type = childElem.getAttribute('data-type');
317 const parentChapter = false;
318 const childId = childElem.getAttribute('data-id');
323 parentChapter: parentChapter,
328 const subPages = childElem.querySelectorAll('[data-type="page"]');
329 for (let i = 0; i < subPages.length; i++) {
331 id: subPages[i].getAttribute('data-id'),
333 parentChapter: childId,
341 * Run the given sort action up the provided sort item.
342 * @param {Element} item
343 * @param {String} action
345 runSortAction(item, action) {
346 const parentItem = item.parentElement.closest('li[data-id]');
347 const parentBook = item.parentElement.closest('[data-type="book"]');
348 moveActions[action].run(item, parentItem, parentBook);
349 this.updateMapInput();
350 this.updateMoveActionStateForAll();
351 item.scrollIntoView({behavior: 'smooth', block: 'nearest'});
356 * Update the state of the available move actions on this item.
357 * @param {Element} item
359 updateMoveActionState(item) {
360 const parentItem = item.parentElement.closest('li[data-id]');
361 const parentBook = item.parentElement.closest('[data-type="book"]');
362 for (const [action, functions] of Object.entries(moveActions)) {
363 const moveButton = item.querySelector(`[data-move="${action}"]`);
364 moveButton.disabled = !functions.active(item, parentItem, parentBook);
368 updateMoveActionStateForAll() {
369 const items = this.container.querySelectorAll('[data-type="chapter"],[data-type="page"]');
370 for (const item of items) {
371 this.updateMoveActionState(item);